From 1e72b7a449a10314d709faf100adb83b2eb1d317 Mon Sep 17 00:00:00 2001 From: Rhett Garber Date: Mon, 27 Sep 2010 13:58:29 -0700 Subject: Python 2.5 compatability --- pystache/template.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index b2abdaa..9006e68 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -2,6 +2,15 @@ import re import cgi import collections +try: + import collections.Callable + def check_callable(it): + return isinstance(it, collections.Callable) +except ImportError: + def check_callable(it): + return hasattr(it, '__call__') + + modifiers = {} def modifier(symbol): """Decorator for associating a function with a Mustache tag modifier. @@ -81,7 +90,7 @@ class Template(object): it = get_or_attr(context, section_name, None) replacer = '' - if it and isinstance(it, collections.Callable): + if it and check_callable(it): replacer = it(inner) elif it and not hasattr(it, '__iter__'): if section[2] != '^': -- cgit v1.2.1 From dedcfbd08bdadf106b3f8162ec7cd3ff221c815b Mon Sep 17 00:00:00 2001 From: Rhett Garber Date: Mon, 27 Sep 2010 14:53:52 -0700 Subject: Testing for section/partial bug --- tests/test_examples.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 9ad6c1e..777bcec 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -12,6 +12,7 @@ from examples.delimiters import Delimiters from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext +from examples.partial_section import PartialSection class TestView(unittest.TestCase): def test_comments(self): @@ -55,7 +56,8 @@ Again, Welcome!""") self.assertEquals(view.render(), """Welcome ------- -Again, Welcome! +## Again, Welcome! ## + """) @@ -68,6 +70,11 @@ Again, Welcome! * Then, surprisingly, it worked the third time. """) + def test_partial_sections(self): + view = PartialSection() + self.assertEquals(view.render(), """Welcome, we're loading partials +This is item aThis is item b""") + def test_nested_context(self): self.assertEquals(NestedContext().render(), "one and foo and two") -- cgit v1.2.1 From dba4ad89772eeffa7a5d4e8acd6d7e82530fba7b Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sun, 6 Feb 2011 11:13:59 -0800 Subject: Adding a submodule reference to the Mustache spec. --- .gitmodules | 3 +++ ext/spec | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 ext/spec diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c55c8e5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/spec"] + path = ext/spec + url = http://github.com/mustache/spec.git diff --git a/ext/spec b/ext/spec new file mode 160000 index 0000000..6287192 --- /dev/null +++ b/ext/spec @@ -0,0 +1 @@ +Subproject commit 62871926ab5789ab6c55f5a1deda359ba5f7b2fa -- cgit v1.2.1 From c19e58c8b0ec74f6605d270b6e445da9b6143c9d Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sun, 6 Feb 2011 15:01:28 -0800 Subject: Adding tests for the Mustache spec. --- tests/test_spec.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_spec.py diff --git a/tests/test_spec.py b/tests/test_spec.py new file mode 100644 index 0000000..22ad0c0 --- /dev/null +++ b/tests/test_spec.py @@ -0,0 +1,39 @@ +import glob +import os.path +import pystache +import unittest +import yaml + +def code_constructor(loader, node): + value = loader.construct_mapping(node) + return eval(value['python'], {}) + +yaml.add_constructor(u'!code', code_constructor) + +specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') +specs = glob.glob(os.path.join(specs, '*.yml')) + +class MustacheSpec(unittest.TestCase): + pass + +def buildTest(testData, spec): + def test(self): + template = testData['template'] + partials = testData.has_key('partials') and test['partials'] or {} + expected = testData['expected'] + data = testData['data'] + self.assertEquals(pystache.render(template, data), expected) + + test.__doc__ = testData['desc'] + test.__name__ = 'test %s (%s)' % (testData['name'], spec) + return test + +for spec in specs: + name = os.path.basename(spec).replace('.yml', '') + + for test in yaml.load(open(spec))['tests']: + test = buildTest(test, name) + setattr(MustacheSpec, test.__name__, test) + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From 11b156c23606c1b2aded407f2a844abc334ed93c Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Mon, 7 Feb 2011 20:20:20 -0800 Subject: Beginning a rework of the parser. --- pystache/template.py | 202 ++++++++++++++++----------------------------------- 1 file changed, 64 insertions(+), 138 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index a457945..afd9528 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,156 +1,82 @@ import re -import cgi -import collections -import os -import copy - -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_re = None - - otag = '{{' - - ctag = '}}' - - def __init__(self, template=None, context=None, **kwargs): + otag, ctag = '{{', '}}' + + def __init__(self, template=None, context={}, **kwargs): from view import View - + self.template = template - + 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) - } - - 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, view): - while True: - match = self.section_re.search(template) - if match is None: - break + tags = {'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag)} + tag = r""" + (?P[\s\S]*?) + (?P[\ \t]*) + %(otag)s \s* + (?: + (?P=) \s* (?P.+?) \s* = | + (?P{) \s* (?P.+?) \s* } | + (?P\W?) \s* (?P[\s\S]+?) + ) + \s* %(ctag)s + """ + self.tag_re = re.compile(tag % tags, re.M | re.X) + + def _parse(self, template, section=None, index=0): + """Parse a template into a syntax tree.""" + + buffer = [] + pos = index - section, section_name, inner = match.group(0, 1, 2) - section_name = section_name.strip() - it = self.view.get(section_name, None) - replacer = '' - - # Callable - if it and isinstance(it, collections.Callable): - replacer = it(inner) - # Dictionary - elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'): - if section[2] != '^': - replacer = self._render_dictionary(inner, it) - # Lists - elif it and hasattr(it, '__iter__'): - if section[2] != '^': - replacer = self._render_list(inner, it) - # Other objects - elif it and isinstance(it, object): - if section[2] != '^': - replacer = self._render_dictionary(inner, it) - # Falsey and Negated or Truthy and Not Negated - elif (not it and section[2] == '^') or (it and section[2] != '^'): - replacer = inner - - template = template.replace(section, replacer) - - return template - - def _render_tags(self, template): while True: - match = self.tag_re.search(template) + match = self.tag_re.search(template, pos) + 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) - template = template.replace(tag, replacement) - - return template - - def _render_dictionary(self, template, context): - self.view.context_list.insert(0, context) - template = Template(template, self.view) - out = template.render() - self.view.context_list.pop(0) - return out - - def _render_list(self, template, listing): - insides = [] - for item in listing: - insides.append(self._render_dictionary(template, item)) - - return ''.join(insides) - - @modifier(None) - def _render_tag(self, tag_name): - raw = self.view.get(tag_name, '') - - # For methods with no return value - if not raw and raw is not 0: - return '' - - return cgi.escape(unicode(raw)) - - @modifier('!') - def _render_comment(self, tag_name): - return '' - - @modifier('>') - def _render_partial(self, template_name): - from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) - template = Template(markup, self.view) - return template.render() - - @modifier('=') - def _change_delimiter(self, tag_name): - """Changes the Mustache delimiter.""" - self.otag, self.ctag = tag_name.split(' ') - self._compile_regexps() - return '' - - @modifier('{') - @modifier('&') - def render_unescaped(self, tag_name): - """Render a tag without escaping it.""" - return unicode(self.view.get(tag_name, '')) - + + # Normalize the captures dictionary. + captures = match.groupdict() + if captures['change'] is not None: + captures.update(tag='=', name=captures['delims']) + elif captures['raw'] is not None: + captures.update(tag='{', name=captures['raw_name']) + + # Save the literal text content. + buffer.append(captures['content']) + pos = match.end() + + # Save the whitespace following the text content. + # TODO: Standalone tags should consume this. + buffer.append(captures['whitespace']) + + # TODO: Process the remaining tag types. + if captures['tag'] is '!': + pass + + # Save the rest of the template. + buffer.append(template[pos:]) + + return buffer + + def _generate(self, parsed, view): + """Convert a parse tree into a fully evaluated template.""" + + # TODO: Handle non-trivial cases. + return ''.join(parsed) + def render(self, encoding=None): - template = self._render_sections(self.template, self.view) - result = self._render_tags(template) - + parsed = self._parse(self.template) + result = self._generate(parsed, self.view) + if encoding is not None: result = result.encode(encoding) - - return result \ No newline at end of file + + return result -- cgit v1.2.1 From 64b7005bcd92e26c6a52a3004d878b401bf32442 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 8 Feb 2011 21:08:14 -0800 Subject: Adding unescaped tags. --- pystache/template.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index afd9528..9bd72e1 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -60,21 +60,23 @@ class Template(object): # TODO: Process the remaining tag types. if captures['tag'] is '!': pass + elif captures['tag'] in ['{', '&']: + def unescapedTag(view): + return view.get(captures['name']) + buffer.append(unescapedTag) # Save the rest of the template. buffer.append(template[pos:]) return buffer - def _generate(self, parsed, view): - """Convert a parse tree into a fully evaluated template.""" - - # TODO: Handle non-trivial cases. - return ''.join(parsed) - def render(self, encoding=None): parsed = self._parse(self.template) - result = self._generate(parsed, self.view) + + def call(x): + return callable(x) and x(self.view) or x + + result = ''.join(map(call, parsed)) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From 0148119219b918e7624e60e137e63e8da7bac4e2 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 8 Feb 2011 21:10:01 -0800 Subject: Promote strings to Unicode. --- pystache/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index 9bd72e1..de3ec6d 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -74,7 +74,7 @@ class Template(object): parsed = self._parse(self.template) def call(x): - return callable(x) and x(self.view) or x + return unicode(callable(x) and x(self.view) or x) result = ''.join(map(call, parsed)) -- cgit v1.2.1 From 03303acc59ad2955c98f919df219f26d2a41a27d Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 8 Feb 2011 21:10:16 -0800 Subject: Adding basic escaped tag support. --- pystache/template.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index de3ec6d..d0b93d2 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,4 +1,5 @@ import re +import cgi class Template(object): tag_re = None @@ -58,12 +59,16 @@ class Template(object): buffer.append(captures['whitespace']) # TODO: Process the remaining tag types. - if captures['tag'] is '!': + if captures['tag'] == '!': pass elif captures['tag'] in ['{', '&']: def unescapedTag(view): return view.get(captures['name']) buffer.append(unescapedTag) + elif captures['tag'] == '': + def escapedTag(view): + return cgi.escape(view.get(captures['name'])) + buffer.append(escapedTag) # Save the rest of the template. buffer.append(template[pos:]) -- cgit v1.2.1 From 50bdc5357c3bc30147a657ec05a2767fe99e9996 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 8 Feb 2011 21:27:23 -0800 Subject: Handling standalone tags. --- pystache/template.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d0b93d2..8e30705 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -54,9 +54,17 @@ class Template(object): buffer.append(captures['content']) pos = match.end() - # Save the whitespace following the text content. - # TODO: Standalone tags should consume this. - buffer.append(captures['whitespace']) + # Standalone (non-interpolation) tags consume the entire line, + # both leading whitespace and trailing newline. + tagBeganLine = (not buffer[-1] or buffer[-1][-1] == '\n') + tagEndedLine = (pos == len(template) or template[pos] == '\n') + interpolationTag = captures['tag'] in ['', '&', '{'] + + if (tagBeganLine and tagEndedLine and not interpolationTag): + pos += 1 + elif captures['whitespace']: + buffer.append(captures['whitespace']) + captures['whitespace'] = '' # TODO: Process the remaining tag types. if captures['tag'] == '!': -- cgit v1.2.1 From 41c92afceb30b7ab5e884890ab8a2f3192ffd68c Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 15 Feb 2011 21:47:19 -0800 Subject: Refactoring the match handling logic. --- pystache/template.py | 73 ++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 8e30705..bdfa2f1 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -43,46 +43,51 @@ class Template(object): if match is None: break - # Normalize the captures dictionary. - captures = match.groupdict() - if captures['change'] is not None: - captures.update(tag='=', name=captures['delims']) - elif captures['raw'] is not None: - captures.update(tag='{', name=captures['raw_name']) - - # Save the literal text content. - buffer.append(captures['content']) - pos = match.end() - - # Standalone (non-interpolation) tags consume the entire line, - # both leading whitespace and trailing newline. - tagBeganLine = (not buffer[-1] or buffer[-1][-1] == '\n') - tagEndedLine = (pos == len(template) or template[pos] == '\n') - interpolationTag = captures['tag'] in ['', '&', '{'] - - if (tagBeganLine and tagEndedLine and not interpolationTag): - pos += 1 - elif captures['whitespace']: - buffer.append(captures['whitespace']) - captures['whitespace'] = '' - - # TODO: Process the remaining tag types. - if captures['tag'] == '!': - pass - elif captures['tag'] in ['{', '&']: - def unescapedTag(view): - return view.get(captures['name']) - buffer.append(unescapedTag) - elif captures['tag'] == '': - def escapedTag(view): - return cgi.escape(view.get(captures['name'])) - buffer.append(escapedTag) + pos = self._handle_match(template, match, buffer) # Save the rest of the template. buffer.append(template[pos:]) return buffer + def _handle_match(self, template, match, buffer): + # Normalize the captures dictionary. + captures = match.groupdict() + if captures['change'] is not None: + captures.update(tag='=', name=captures['delims']) + elif captures['raw'] is not None: + captures.update(tag='{', name=captures['raw_name']) + + # Save the literal text content. + buffer.append(captures['content']) + pos = match.end() + + # Standalone (non-interpolation) tags consume the entire line, + # both leading whitespace and trailing newline. + tagBeganLine = (not buffer[-1] or buffer[-1][-1] == '\n') + tagEndedLine = (pos == len(template) or template[pos] == '\n') + interpolationTag = captures['tag'] in ['', '&', '{'] + + if (tagBeganLine and tagEndedLine and not interpolationTag): + pos += 1 + elif captures['whitespace']: + buffer.append(captures['whitespace']) + captures['whitespace'] = '' + + # TODO: Process the remaining tag types. + print captures['name'] + fetch = lambda view: unicode(view.get(captures['name'])) + if captures['tag'] == '!': + pass + elif captures['tag'] in ['{', '&']: + buffer.append(fetch) + elif captures['tag'] == '': + buffer.append(lambda view: cgi.escape(fetch(view), True)) + else: + print 'Error!' + + return pos + def render(self, encoding=None): parsed = self._parse(self.template) -- cgit v1.2.1 From 3fe3ab95e9b42426d383b4da31261265ff9c60c7 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Wed, 16 Feb 2011 18:44:19 -0800 Subject: Adding support for Set Delimiters tags. --- pystache/template.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pystache/template.py b/pystache/template.py index bdfa2f1..8c6b42a 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -79,6 +79,10 @@ class Template(object): fetch = lambda view: unicode(view.get(captures['name'])) if captures['tag'] == '!': pass + elif captures['tag'] == '=': + print '"', captures['name'], '"' + self.otag, self.ctag = captures['name'].split() + self._compile_regexps() elif captures['tag'] in ['{', '&']: buffer.append(fetch) elif captures['tag'] == '': -- cgit v1.2.1 From bd24077a50d7dca22159da474502ffd4c307ea6f Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sun, 20 Feb 2011 20:54:36 -0800 Subject: Cleaning up the fetch routine a bit. Also deleting a bit of unintended diagnostic code. --- pystache/template.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 8c6b42a..85d0e89 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -75,20 +75,19 @@ class Template(object): captures['whitespace'] = '' # TODO: Process the remaining tag types. - print captures['name'] - fetch = lambda view: unicode(view.get(captures['name'])) + fetch = lambda view: view.get(captures['name']) + if captures['tag'] == '!': pass elif captures['tag'] == '=': - print '"', captures['name'], '"' self.otag, self.ctag = captures['name'].split() self._compile_regexps() elif captures['tag'] in ['{', '&']: - buffer.append(fetch) + buffer.append(lambda view: unicode(fetch(view))) elif captures['tag'] == '': - buffer.append(lambda view: cgi.escape(fetch(view), True)) + buffer.append(lambda view: cgi.escape(unicode(fetch(view)), True)) else: - print 'Error!' + raise return pos -- cgit v1.2.1 From 1b31b0dc4ac94e082a8ea04dbe4928f52a76255c Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Mon, 21 Feb 2011 12:41:07 -0800 Subject: Adding basic support for partials. --- pystache/template.py | 4 +++- pystache/view.py | 4 ++++ tests/test_spec.py | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 85d0e89..d698913 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -82,12 +82,14 @@ class Template(object): elif captures['tag'] == '=': self.otag, self.ctag = captures['name'].split() self._compile_regexps() + elif captures['tag'] == '>': + buffer += self._parse(self.view.partial(captures['name'])) elif captures['tag'] in ['{', '&']: buffer.append(lambda view: unicode(fetch(view))) elif captures['tag'] == '': buffer.append(lambda view: cgi.escape(unicode(fetch(view)), True)) else: - raise + raise Exception("'%s' is an unrecognized type!" % (captures['tag'])) return pos diff --git a/pystache/view.py b/pystache/view.py index 925998e..432199e 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -49,6 +49,10 @@ class View(object): return self.template + def partial(self, name): + from pystache import Loader + return Loader().load_template(name, self.template_path, encoding=self.template_encoding, extension=self.template_extension) + def _get_template_name(self, template_name=None): """TemplatePartial => template_partial Takes a string but defaults to using the current class' name or diff --git a/tests/test_spec.py b/tests/test_spec.py index 22ad0c0..9d10721 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,6 +1,7 @@ import glob import os.path import pystache +from pystache import Loader import unittest import yaml @@ -19,10 +20,20 @@ class MustacheSpec(unittest.TestCase): def buildTest(testData, spec): def test(self): template = testData['template'] - partials = testData.has_key('partials') and test['partials'] or {} + partials = testData.has_key('partials') and testData['partials'] or {} expected = testData['expected'] data = testData['data'] - self.assertEquals(pystache.render(template, data), expected) + files = [] + + try: + for key in partials.keys(): + filename = "%s.%s" % (key, Loader.template_extension) + files.append(os.path.join(Loader.template_path, filename)) + p = open(files[-1], 'w') + p.write(partials[key]) + self.assertEquals(pystache.render(template, data), expected) + finally: + [os.remove(f) for f in files] test.__doc__ = testData['desc'] test.__name__ = 'test %s (%s)' % (testData['name'], spec) -- cgit v1.2.1 From 00c9393593ceda7c306f1187d4076be0579fe510 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 15 Mar 2011 20:22:18 -0700 Subject: Refactoring interpolation tags. --- pystache/template.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d698913..252d75c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,6 +1,16 @@ import re import cgi +def escapedTag(name): + def func(view): + return cgi.escape(unicode(view.get(name)), True) + return func + +def unescapedTag(name): + def func(view): + return unicode(view.get(name)) + return func + class Template(object): tag_re = None otag, ctag = '{{', '}}' @@ -74,20 +84,18 @@ class Template(object): buffer.append(captures['whitespace']) captures['whitespace'] = '' - # TODO: Process the remaining tag types. - fetch = lambda view: view.get(captures['name']) - + name = captures['name'] if captures['tag'] == '!': pass elif captures['tag'] == '=': - self.otag, self.ctag = captures['name'].split() + self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - buffer += self._parse(self.view.partial(captures['name'])) + buffer += self._parse(self.view.partial(name)) elif captures['tag'] in ['{', '&']: - buffer.append(lambda view: unicode(fetch(view))) + buffer.append(unescapedTag(name)) elif captures['tag'] == '': - buffer.append(lambda view: cgi.escape(unicode(fetch(view)), True)) + buffer.append(escapedTag(name)) else: raise Exception("'%s' is an unrecognized type!" % (captures['tag'])) -- cgit v1.2.1 From 327c883762da11b6ea49cda59296f88b65a23d35 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 15 Mar 2011 22:44:37 -0700 Subject: *Very* rough first pass at section tags. --- pystache/template.py | 60 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 252d75c..e1c8929 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,6 +1,34 @@ import re import cgi +def call(view): + def _(x): + return unicode(callable(x) and x(view) or x) + return _ + +def sectionTag(name, template, delims): + def func(view): + if not view.get(name): + return '' + print template + tmpl = Template(template) + tmpl.view = view + (tmpl.otag, tmpl.ctag) = delims + view.context_list = [view.get(name)] + view.context_list + string = ''.join(map(call(view), tmpl._parse())) + return string + return func + +def inverseTag(name, template, delims): + def func(view): + if view.get(name): + return '' + tmpl = Template(template) + tmpl.view = view + (tmpl.otag, tmpl.ctag) = delims + return ''.join(map(call(view), tmpl._parse())) + return func + def escapedTag(name): def func(view): return cgi.escape(unicode(view.get(name)), True) @@ -11,6 +39,11 @@ def unescapedTag(name): return unicode(view.get(name)) return func +class EndOfSection(Exception): + def __init__(self, template, position): + self.template = template + self.position = position + class Template(object): tag_re = None otag, ctag = '{{', '}}' @@ -41,9 +74,10 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) - def _parse(self, template, section=None, index=0): + def _parse(self, template=None, section=None, index=0): """Parse a template into a syntax tree.""" + template = template != None and template or self.template buffer = [] pos = index @@ -53,14 +87,14 @@ class Template(object): if match is None: break - pos = self._handle_match(template, match, buffer) + pos = self._handle_match(template, match, buffer, index) # Save the rest of the template. buffer.append(template[pos:]) return buffer - def _handle_match(self, template, match, buffer): + def _handle_match(self, template, match, buffer, index): # Normalize the captures dictionary. captures = match.groupdict() if captures['change'] is not None: @@ -92,6 +126,17 @@ class Template(object): self._compile_regexps() elif captures['tag'] == '>': buffer += self._parse(self.view.partial(name)) + elif captures['tag'] in ['#', '^']: + try: + self._parse(template, name, pos) + except EndOfSection as e: + tmpl = e.template + pos = e.position + + tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] + buffer.append(tag(name, tmpl, (self.otag, self.ctag))) + elif captures['tag'] == '/': + raise EndOfSection(template[index:match.end('whitespace')], pos) elif captures['tag'] in ['{', '&']: buffer.append(unescapedTag(name)) elif captures['tag'] == '': @@ -102,13 +147,8 @@ class Template(object): return pos def render(self, encoding=None): - parsed = self._parse(self.template) - - def call(x): - return unicode(callable(x) and x(self.view) or x) - - result = ''.join(map(call, parsed)) - + parsed = self._parse() + result = ''.join(map(call(self.view), parsed)) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From 0f986093ba1e64841b12ac85a20bab0e2e8ec8d7 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Wed, 16 Mar 2011 01:16:57 -0700 Subject: Significant improvements to Section handling. --- pystache/template.py | 54 +++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index e1c8929..7301d7d 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,32 +1,34 @@ import re import cgi -def call(view): - def _(x): - return unicode(callable(x) and x(view) or x) - return _ +def call(view, x): + if callable(x): + x = x(view) + return unicode(x) -def sectionTag(name, template, delims): +def sectionTag(name, parsed, template, delims): def func(view): - if not view.get(name): + data = view.get(name) + if not data: return '' - print template - tmpl = Template(template) - tmpl.view = view - (tmpl.otag, tmpl.ctag) = delims - view.context_list = [view.get(name)] + view.context_list - string = ''.join(map(call(view), tmpl._parse())) - return string + elif type(data) not in [list, tuple]: + data = [ data ] + + parts = [] + for element in data: + view.context_list.insert(0, element) + parts.append(''.join(map(call, [view] * len(parsed), parsed))) + del view.context_list[0] + + return ''.join(parts) return func -def inverseTag(name, template, delims): +def inverseTag(name, parsed, template, delims): def func(view): - if view.get(name): + data = view.get(name) + if data: return '' - tmpl = Template(template) - tmpl.view = view - (tmpl.otag, tmpl.ctag) = delims - return ''.join(map(call(view), tmpl._parse())) + return ''.join(map(call, [view] * len(parsed), parsed)) return func def escapedTag(name): @@ -40,7 +42,8 @@ def unescapedTag(name): return func class EndOfSection(Exception): - def __init__(self, template, position): + def __init__(self, buffer, template, position): + self.buffer = buffer self.template = template self.position = position @@ -105,10 +108,11 @@ class Template(object): # Save the literal text content. buffer.append(captures['content']) pos = match.end() + tagPos = match.end('content') # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. - tagBeganLine = (not buffer[-1] or buffer[-1][-1] == '\n') + tagBeganLine = not tagPos or template[tagPos - 1] == '\n' tagEndedLine = (pos == len(template) or template[pos] == '\n') interpolationTag = captures['tag'] in ['', '&', '{'] @@ -116,6 +120,7 @@ class Template(object): pos += 1 elif captures['whitespace']: buffer.append(captures['whitespace']) + tagPos += len(captures['whitespace']) captures['whitespace'] = '' name = captures['name'] @@ -130,13 +135,14 @@ class Template(object): try: self._parse(template, name, pos) except EndOfSection as e: + bufr = e.buffer tmpl = e.template pos = e.position tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] - buffer.append(tag(name, tmpl, (self.otag, self.ctag))) + buffer.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': - raise EndOfSection(template[index:match.end('whitespace')], pos) + raise EndOfSection(buffer, template[index:tagPos], pos) elif captures['tag'] in ['{', '&']: buffer.append(unescapedTag(name)) elif captures['tag'] == '': @@ -148,7 +154,7 @@ class Template(object): def render(self, encoding=None): parsed = self._parse() - result = ''.join(map(call(self.view), parsed)) + result = ''.join(map(call, [self.view] * len(parsed), parsed)) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From c94e2840714646c06f0f0923b7f6efff3384b60b Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Wed, 16 Mar 2011 09:14:14 -0700 Subject: Fixing a couple basic errors with lambdas. --- pystache/template.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 7301d7d..3657297 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,14 +1,19 @@ import re import cgi +import inspect def call(view, x): if callable(x): - x = x(view) + (args, _, _, _) = inspect.getargspec(x) + if len(args) is 0: + x = x() + elif len(args) is 1 and args[0] == 'self': + x = x(view) return unicode(x) def sectionTag(name, parsed, template, delims): - def func(view): - data = view.get(name) + def func(self): + data = self.get(name) if not data: return '' elif type(data) not in [list, tuple]: @@ -16,29 +21,30 @@ def sectionTag(name, parsed, template, delims): parts = [] for element in data: - view.context_list.insert(0, element) - parts.append(''.join(map(call, [view] * len(parsed), parsed))) - del view.context_list[0] + self.context_list.insert(0, element) + parts.append(''.join(map(call, [self] * len(parsed), parsed))) + del self.context_list[0] return ''.join(parts) return func def inverseTag(name, parsed, template, delims): - def func(view): - data = view.get(name) + def func(self): + data = self.get(name) if data: return '' - return ''.join(map(call, [view] * len(parsed), parsed)) + return ''.join(map(call, [self] * len(parsed), parsed)) return func def escapedTag(name): - def func(view): - return cgi.escape(unicode(view.get(name)), True) + fetch = unescapedTag(name) + def func(self): + return cgi.escape(fetch(self), True) return func def unescapedTag(name): - def func(view): - return unicode(view.get(name)) + def func(self): + return unicode(call(self, self.get(name))) return func class EndOfSection(Exception): @@ -148,7 +154,7 @@ class Template(object): elif captures['tag'] == '': buffer.append(escapedTag(name)) else: - raise Exception("'%s' is an unrecognized type!" % (captures['tag'])) + raise Exception("'%s' is an unrecognized type!" % captures['tag']) return pos -- cgit v1.2.1 From ce5f66ff949c35936f18e286148fb1f19be1596e Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 09:16:23 -0700 Subject: Enabling the use of lambdas for Sections. --- pystache/template.py | 16 ++++++++++++++-- pystache/view.py | 6 +----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 3657297..45dbf30 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -2,27 +2,39 @@ import re import cgi import inspect -def call(view, x): +def call(view, x, template=None): if callable(x): (args, _, _, _) = inspect.getargspec(x) + print args if len(args) is 0: x = x() elif len(args) is 1 and args[0] == 'self': x = x(view) + elif len(args) is 1: + x = x(template) + else: + x = x(view, template) return unicode(x) def sectionTag(name, parsed, template, delims): def func(self): data = self.get(name) + ast = parsed if not data: return '' + elif callable(data): + tmpl = Template(call(self, data, template)) + tmpl.otag, tmpl.ctag = delims + tmpl._compile_regexps() + ast = tmpl._parse() + data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] parts = [] for element in data: self.context_list.insert(0, element) - parts.append(''.join(map(call, [self] * len(parsed), parsed))) + parts.append(''.join(map(call, [self] * len(ast), ast))) del self.context_list[0] return ''.join(parts) diff --git a/pystache/view.py b/pystache/view.py index 432199e..d854064 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -35,11 +35,7 @@ class View(object): self.context_list = [context] def get(self, attr, default=None): - attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) - if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: - return attr() - else: - return attr + return get_or_attr(self.context_list, attr, getattr(self, attr, default)) def get_template(self, template_name): if not self.template: -- cgit v1.2.1 From 9de3868077d64ac0e6b461752d3d3b2404c143ea Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 09:17:05 -0700 Subject: Updating the ext/spec submodule to v1.0.2. --- ext/spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/spec b/ext/spec index 6287192..b3223d9 160000 --- a/ext/spec +++ b/ext/spec @@ -1 +1 @@ -Subproject commit 62871926ab5789ab6c55f5a1deda359ba5f7b2fa +Subproject commit b3223d95727a90f6011a29a9bc1e1b103e3e3304 -- cgit v1.2.1 From 88f604c6303ee6df9536c8c69a513251224c4184 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 09:24:01 -0700 Subject: Fixing errors relating to Windows-style newlines. --- pystache/template.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 45dbf30..d2122b7 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -130,12 +130,15 @@ class Template(object): # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. - tagBeganLine = not tagPos or template[tagPos - 1] == '\n' - tagEndedLine = (pos == len(template) or template[pos] == '\n') + tagBeganLine = not tagPos or template[tagPos - 1] in ['\r', '\n'] + tagEndedLine = (pos == len(template) or template[pos] in ['\r', '\n']) interpolationTag = captures['tag'] in ['', '&', '{'] if (tagBeganLine and tagEndedLine and not interpolationTag): - pos += 1 + if pos < len(template): + pos += template[pos] == '\r' and 1 or 0 + if pos < len(template): + pos += template[pos] == '\n' and 1 or 0 elif captures['whitespace']: buffer.append(captures['whitespace']) tagPos += len(captures['whitespace']) -- cgit v1.2.1 From 5c9bdd56c49cc6730058ad2614b050c8f2ec8b1f Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 09:44:10 -0700 Subject: Specifying a default of '' for context misses. --- pystache/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index d854064..f485a55 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -3,7 +3,7 @@ import os.path import re from types import * -def get_or_attr(context_list, name, default=None): +def get_or_attr(context_list, name, default): if not context_list: return default @@ -34,7 +34,7 @@ class View(object): self.context_list = [context] - def get(self, attr, default=None): + def get(self, attr, default=''): return get_or_attr(self.context_list, attr, getattr(self, attr, default)) def get_template(self, template_name): -- cgit v1.2.1 From c39b8b847516720cadbfe7c055fa7389da047eed Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 09:50:13 -0700 Subject: Fixing some basic Partial rendering problems. --- pystache/template.py | 5 +++-- tests/test_spec.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d2122b7..c1835a4 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -5,7 +5,6 @@ import inspect def call(view, x, template=None): if callable(x): (args, _, _, _) = inspect.getargspec(x) - print args if len(args) is 0: x = x() elif len(args) is 1 and args[0] == 'self': @@ -151,7 +150,9 @@ class Template(object): self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - buffer += self._parse(self.view.partial(name)) + tmpl = Template(self.view.partial(name)) + tmpl.view = self.view + buffer += tmpl._parse() elif captures['tag'] in ['#', '^']: try: self._parse(template, name, pos) diff --git a/tests/test_spec.py b/tests/test_spec.py index 9d10721..125e411 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -31,6 +31,7 @@ def buildTest(testData, spec): files.append(os.path.join(Loader.template_path, filename)) p = open(files[-1], 'w') p.write(partials[key]) + p.close() self.assertEquals(pystache.render(template, data), expected) finally: [os.remove(f) for f in files] -- cgit v1.2.1 From be3d02d926427c40b61f47c233c556d469031bab Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 21:05:55 -0700 Subject: Fix the partial recursion error. --- pystache/template.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index c1835a4..85225c9 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -15,6 +15,14 @@ def call(view, x, template=None): x = x(view, template) return unicode(x) +def partialTag(name): + def func(self): + tmpl = Template(self.partial(name)) + tmpl.view = self + parsed = tmpl._parse() + return ''.join(map(call, [self] * len(parsed), parsed)) + return func + def sectionTag(name, parsed, template, delims): def func(self): data = self.get(name) @@ -150,9 +158,7 @@ class Template(object): self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - tmpl = Template(self.view.partial(name)) - tmpl.view = self.view - buffer += tmpl._parse() + buffer.append(partialTag(name)) elif captures['tag'] in ['#', '^']: try: self._parse(template, name, pos) -- cgit v1.2.1 From 7f6da416fa648e612376056a131d076ab9d344b7 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sat, 19 Mar 2011 21:24:37 -0700 Subject: Handling partial indentation. --- pystache/template.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 85225c9..8e5a445 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -15,11 +15,13 @@ def call(view, x, template=None): x = x(view, template) return unicode(x) -def partialTag(name): +def partialTag(name, indentation=''): def func(self): - tmpl = Template(self.partial(name)) - tmpl.view = self - parsed = tmpl._parse() + nonblank = re.compile(r'^(.)', re.M) + template = re.sub(nonblank, indentation + r'\1', self.partial(name)) + template = Template(template) + template.view = self + parsed = template._parse() return ''.join(map(call, [self] * len(parsed), parsed)) return func @@ -158,7 +160,7 @@ class Template(object): self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - buffer.append(partialTag(name)) + buffer.append(partialTag(name, captures['whitespace'])) elif captures['tag'] in ['#', '^']: try: self._parse(template, name, pos) -- cgit v1.2.1 From 23bd00539f7d1c731e8dbcf2d516edb9e32779e8 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sun, 20 Mar 2011 10:12:25 -0700 Subject: Refactoring some rendering-related behavior. This now passes the entire Mustache spec v1.0.2. --- pystache/template.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 8e5a445..dcd786e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -15,14 +15,26 @@ def call(view, x, template=None): x = x(view, template) return unicode(x) +def parse(template, view, delims=('{{', '}}')): + tmpl = Template(template) + tmpl.view = view + tmpl.otag, tmpl.ctag = delims + tmpl._compile_regexps() + return tmpl._parse() + +def renderParseTree(parsed, view, template): + n = len(parsed) + return ''.join(map(call, [view] * n, parsed, [template] * n)) + +def render(template, view, delims=('{{', '}}')): + parseTree = parse(template, view, delims) + return renderParseTree(parseTree, view, template) + def partialTag(name, indentation=''): def func(self): nonblank = re.compile(r'^(.)', re.M) template = re.sub(nonblank, indentation + r'\1', self.partial(name)) - template = Template(template) - template.view = self - parsed = template._parse() - return ''.join(map(call, [self] * len(parsed), parsed)) + return render(template, self) return func def sectionTag(name, parsed, template, delims): @@ -32,10 +44,7 @@ def sectionTag(name, parsed, template, delims): if not data: return '' elif callable(data): - tmpl = Template(call(self, data, template)) - tmpl.otag, tmpl.ctag = delims - tmpl._compile_regexps() - ast = tmpl._parse() + ast = parse(call(self, data, template), self, delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -43,7 +52,7 @@ def sectionTag(name, parsed, template, delims): parts = [] for element in data: self.context_list.insert(0, element) - parts.append(''.join(map(call, [self] * len(ast), ast))) + parts.append(renderParseTree(ast, self, delims)) del self.context_list[0] return ''.join(parts) @@ -54,18 +63,18 @@ def inverseTag(name, parsed, template, delims): data = self.get(name) if data: return '' - return ''.join(map(call, [self] * len(parsed), parsed)) + return renderParseTree(parsed, self, delims) return func -def escapedTag(name): - fetch = unescapedTag(name) +def escapedTag(name, delims): + fetch = unescapedTag(name, delims) def func(self): return cgi.escape(fetch(self), True) return func -def unescapedTag(name): +def unescapedTag(name, delims): def func(self): - return unicode(call(self, self.get(name))) + return unicode(render(call(self, self.get(name)), self, delims)) return func class EndOfSection(Exception): @@ -174,17 +183,16 @@ class Template(object): elif captures['tag'] == '/': raise EndOfSection(buffer, template[index:tagPos], pos) elif captures['tag'] in ['{', '&']: - buffer.append(unescapedTag(name)) + buffer.append(unescapedTag(name, (self.otag, self.ctag))) elif captures['tag'] == '': - buffer.append(escapedTag(name)) + buffer.append(escapedTag(name, (self.otag, self.ctag))) else: raise Exception("'%s' is an unrecognized type!" % captures['tag']) return pos def render(self, encoding=None): - parsed = self._parse() - result = ''.join(map(call, [self.view] * len(parsed), parsed)) + result = render(self.template, self.view) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From e06bed513dd380bb7760a2bf69af2f7bc0edf405 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Sun, 20 Mar 2011 11:50:50 -0700 Subject: Compatible with Mustache spec v1.0.3. --- ext/spec | 2 +- pystache/template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/spec b/ext/spec index b3223d9..48c933b 160000 --- a/ext/spec +++ b/ext/spec @@ -1 +1 @@ -Subproject commit b3223d95727a90f6011a29a9bc1e1b103e3e3304 +Subproject commit 48c933b0bb780875acbfd15816297e263c53d6f7 diff --git a/pystache/template.py b/pystache/template.py index dcd786e..03a691e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -74,7 +74,7 @@ def escapedTag(name, delims): def unescapedTag(name, delims): def func(self): - return unicode(render(call(self, self.get(name)), self, delims)) + return unicode(render(call(self, self.get(name)), self)) return func class EndOfSection(Exception): -- cgit v1.2.1 From a92a5359afd67234aa71185a3c8f7bdf43c8d729 Mon Sep 17 00:00:00 2001 From: Heliodor Jalba Date: Thu, 21 Jul 2011 10:36:54 -0400 Subject: Added test for spacing around section tags on a single line. Fixed the problem indicated by the test: a section's contents were being left trimmed. --- pystache/template.py | 2 +- tests/test_pystache.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index 563d830..45be299 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,7 +62,7 @@ class Template(object): 'ctag': re.escape(self.ctag) } - section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s\s*(.+?\s*)%(otag)s/\1%(ctag)s" + section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s" self.section_re = re.compile(section % tags, re.M|re.S) tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" diff --git a/tests/test_pystache.py b/tests/test_pystache.py index c04489b..16d457f 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -75,5 +75,10 @@ class TestPystache(unittest.TestCase): ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") + def test_spacing(self): + template = "first{{#spacing}} second {{/spacing}}third" + ret = pystache.render(template, {"spacing": True}) + self.assertEquals(ret, "first second third") + if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 7667a95e285bbf162e2425045999e07b7bfea5f9 Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 28 Jul 2011 15:05:19 +0200 Subject: added command line utility to render templates; partials can be passed as a dictionary --- pystache/commands.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 pystache/commands.py diff --git a/pystache/commands.py b/pystache/commands.py new file mode 100644 index 0000000..df208a3 --- /dev/null +++ b/pystache/commands.py @@ -0,0 +1,30 @@ +from pystache import Template +import argparse +import json +from loader import Loader + +def main(): + parser = argparse.ArgumentParser(description='Render a mustache template with the given context.') + parser.add_argument('template', help='A filename or a template code.') + parser.add_argument('context', help='A filename or a JSON string') + args = parser.parse_args() + + if args.template.endswith('.mustache'): + args.template = args.template[:-9] + + try: + template = Loader().load_template(args.template) + except IOError: + template = args.template + + try: + context = json.load(open(args.context)) + except IOError: + context = json.loads(args.context) + + print(Template(template, context).render()) + + +if __name__=='__main__': + main() + -- cgit v1.2.1 From 4c22f137697c0a3437a6d867f44e958d6f6969f2 Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 4 Aug 2011 16:39:43 +0200 Subject: ok fail, forgot to add files. --- pystache/__init__.py | 1 + pystache/loader.py | 11 ++++++----- pystache/template.py | 14 +++++++++----- pystache/view.py | 3 ++- setup.py | 2 ++ 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pystache/__init__.py b/pystache/__init__.py index 314c5c8..32202f0 100644 --- a/pystache/__init__.py +++ b/pystache/__init__.py @@ -6,3 +6,4 @@ def render(template, context=None, **kwargs): context = context and context.copy() or {} context.update(kwargs) return Template(template, context).render() + diff --git a/pystache/loader.py b/pystache/loader.py index 63b7ee6..53cc32a 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -2,14 +2,15 @@ import os class Loader(object): - template_extension = 'mustache' - template_path = '.' - template_encoding = None + def __init__(self, paths='.', extension='mustache', encoding=None): + self.template_paths = paths + self.template_extension = extension + self.template_encoding = encoding def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): '''Returns the template string from a file or throws IOError if it non existent''' if None == template_dirs: - template_dirs = self.template_path + template_dirs = self.template_paths if encoding is not None: self.template_encoding = encoding @@ -44,4 +45,4 @@ class Loader(object): finally: f.close() - return template \ No newline at end of file + return template diff --git a/pystache/template.py b/pystache/template.py index 563d830..252e49e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -45,10 +45,11 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, context=None, **kwargs): + def __init__(self, template=None, context=None, partials=None, **kwargs): from view import View self.template = template + self.partials = partials if kwargs: context.update(kwargs) @@ -118,7 +119,7 @@ class Template(object): def _render_dictionary(self, template, context): self.view.context_list.insert(0, context) - template = Template(template, self.view) + template = Template(template, self.view, self.partials) out = template.render() self.view.context_list.pop(0) return out @@ -149,9 +150,12 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): - from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) - template = Template(markup, self.view) + if self.partials: + template = Template(self.partials[template_name], self.view, self.partials) + else: + from pystache import Loader + markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) + template = Template(markup, self.view) return template.render() @modifiers.set('=') diff --git a/pystache/view.py b/pystache/view.py index 925998e..20e986a 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -91,4 +91,5 @@ class View(object): raise AttributeError("Attribute '%s' does not exist in View" % attr) def __str__(self): - return self.render() \ No newline at end of file + return self.render() + diff --git a/setup.py b/setup.py index ad9981f..62b887b 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,8 @@ setup(name='pystache', url='http://github.com/defunkt/pystache', packages=['pystache'], license='MIT', + entry_points = { + 'console_scripts': ['pystache=pystache.commands:main']}, classifiers = ( "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", -- cgit v1.2.1 From 8ca7408f6948f7a7214897d4a31b0a3aabaf8216 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:00:44 -0400 Subject: code in __init__.py is evil --- pystache/__init__.py | 9 +-------- pystache/core.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 pystache/core.py diff --git a/pystache/__init__.py b/pystache/__init__.py index 314c5c8..5d20240 100644 --- a/pystache/__init__.py +++ b/pystache/__init__.py @@ -1,8 +1 @@ -from pystache.template import Template -from pystache.view import View -from pystache.loader import Loader - -def render(template, context=None, **kwargs): - context = context and context.copy() or {} - context.update(kwargs) - return Template(template, context).render() +from core import * \ No newline at end of file diff --git a/pystache/core.py b/pystache/core.py new file mode 100644 index 0000000..3d1b0b8 --- /dev/null +++ b/pystache/core.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from .template import Template +from .view import View +from .loader import Loader + +__all__ = ['Template', 'View', 'Loader', 'render'] + + +def render(template, context=None, **kwargs): + + context = context and context.copy() or {} + context.update(kwargs) + + return Template(template, context).render() -- cgit v1.2.1 From 20bec630efed85dd0e4eaa751da2af3db7f64f3e Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:01:01 -0400 Subject: style and import changes --- pystache/loader.py | 44 ++++++++++++++++-------- pystache/template.py | 62 ++++++++++++++++++++++++--------- pystache/view.py | 97 +++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 144 insertions(+), 59 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 63b7ee6..58fb0fb 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -1,47 +1,63 @@ +# -*- coding: utf-8 -*- + +""" +pystache.loader +~~~~~~~~~~~~~~~ + +This module provides Pystache's Loader class. +""" + import os + class Loader(object): - + template_extension = 'mustache' template_path = '.' template_encoding = None - + def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): - '''Returns the template string from a file or throws IOError if it non existent''' + """Returns the template string from a file, or throws IOError + if it is non-existent. + """ + if None == template_dirs: template_dirs = self.template_path - + if encoding is not None: self.template_encoding = encoding - + if extension is not None: self.template_extension = extension - + file_name = template_name + '.' + self.template_extension - # Given a single directory we'll load from it + # Given a single directory, we'll load from it. if isinstance(template_dirs, basestring): file_path = os.path.join(template_dirs, file_name) return self._load_template_file(file_path) - - # Given a list of directories we'll check each for our file + + # Given a list of directories, we'll check each for our file. for path in template_dirs: file_path = os.path.join(path, file_name) if os.path.exists(file_path): return self._load_template_file(file_path) - + raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),)) - + + def _load_template_file(self, file_path): - '''Loads and returns the template file from disk''' + """Loads and returns the template file from disk.""" + f = open(file_path, 'r') - + try: template = f.read() if self.template_encoding: template = unicode(template, self.template_encoding) + finally: f.close() - + return template \ No newline at end of file diff --git a/pystache/template.py b/pystache/template.py index 563d830..4fbb02c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,14 +1,15 @@ +# -*- coding: utf-8 -*- + import re import cgi import collections -import os -import copy + +from .loader import Loader try: import markupsafe escape = markupsafe.escape literal = markupsafe.Markup - except ImportError: escape = lambda x: cgi.escape(unicode(x)) literal = unicode @@ -18,15 +19,15 @@ class Modifiers(dict): """Dictionary with a decorator for assigning functions to keys.""" def set(self, key): - """ - Decorator function to set the given key to the decorated function. - - >>> modifiers = {} - >>> @modifiers.set('P') - ... def render_tongue(self, tag_name=None, context=None): - ... return ":P %s" % tag_name - >>> modifiers - {'P': } + """Decorator function to set the given key to + the decorated function. + + >>> modifiers = {} + >>> @modifiers.set('P') + ... def render_tongue(self, tag_name=None, context=None): + ... return ":P %s" % tag_name + >>> modifiers + {'P': } """ def setter(func): @@ -35,18 +36,17 @@ class Modifiers(dict): return setter + class Template(object): tag_re = None - otag = '{{' - ctag = '}}' modifiers = Modifiers() def __init__(self, template=None, context=None, **kwargs): - from view import View + from .view import View self.template = template @@ -56,6 +56,7 @@ class Template(object): self.view = context if isinstance(context, View) else View(context=context) self._compile_regexps() + def _compile_regexps(self): tags = { 'otag': re.escape(self.otag), @@ -68,6 +69,7 @@ class Template(object): tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) + def _render_sections(self, template, view): while True: match = self.section_re.search(template) @@ -82,18 +84,22 @@ class Template(object): # Callable if it and isinstance(it, collections.Callable): replacer = it(inner) + # Dictionary elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'): if section[2] != '^': replacer = self._render_dictionary(inner, it) + # Lists elif it and hasattr(it, '__iter__'): if section[2] != '^': replacer = self._render_list(inner, it) + # Other objects elif it and isinstance(it, object): if section[2] != '^': replacer = self._render_dictionary(inner, it) + # Falsey and Negated or Truthy and Not Negated elif (not it and section[2] == '^') or (it and section[2] != '^'): replacer = self._render_dictionary(inner, it) @@ -102,6 +108,7 @@ class Template(object): return template + def _render_tags(self, template): while True: match = self.tag_re.search(template) @@ -116,22 +123,32 @@ class Template(object): return template + def _render_dictionary(self, template, context): + self.view.context_list.insert(0, context) + template = Template(template, self.view) out = template.render() + self.view.context_list.pop(0) + return out + def _render_list(self, template, listing): + insides = [] + for item in listing: insides.append(self._render_dictionary(template, item)) return ''.join(insides) + @modifiers.set(None) def _render_tag(self, tag_name): + raw = self.view.get(tag_name, '') # For methods with no return value @@ -143,30 +160,41 @@ class Template(object): return escape(raw) + @modifiers.set('!') def _render_comment(self, tag_name): return '' + @modifiers.set('>') def _render_partial(self, template_name): - from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) + + markup = Loader().load_template( + template_name, + self.view.template_path, + encoding=self.view.template_encoding) template = Template(markup, self.view) return template.render() + @modifiers.set('=') def _change_delimiter(self, tag_name): """Changes the Mustache delimiter.""" + self.otag, self.ctag = tag_name.split(' ') self._compile_regexps() + return '' + @modifiers.set('{') @modifiers.set('&') def render_unescaped(self, tag_name): """Render a tag without escaping it.""" + return literal(self.view.get(tag_name, '')) + def render(self, encoding=None): template = self._render_sections(self.template, self.view) result = self._render_tags(template) diff --git a/pystache/view.py b/pystache/view.py index 925998e..0e4732f 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -1,59 +1,70 @@ -from pystache import Template -import os.path +# -*- coding: utf-8 -*- + +""" +pystache.loader +~~~~~~~~~~~~~~~ + +This module provides Pystache's views. +""" + import re -from types import * +from types import UnboundMethodType + +from .loader import Loader +from .template import Template -def get_or_attr(context_list, name, default=None): - if not context_list: - return default - for obj in context_list: - try: - return obj[name] - except KeyError: - pass - except: - try: - return getattr(obj, name) - except AttributeError: - pass - return default class View(object): - + """A Pystache view.""" + template_name = None template_path = None template = None template_encoding = None template_extension = 'mustache' - + def __init__(self, template=None, context=None, **kwargs): + self.template = template + context = context or {} - context.update(**kwargs) + context.update(kwargs) self.context_list = [context] - + + def get(self, attr, default=None): + attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) + if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: return attr() else: return attr - + + def get_template(self, template_name): + if not self.template: - from pystache import Loader + template_name = self._get_template_name(template_name) - self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension) - + + self.template = Loader().load_template( + template_name, + self.template_path, + encoding=self.template_encoding, + extension=self.template_extension) + return self.template + def _get_template_name(self, template_name=None): """TemplatePartial => template_partial Takes a string but defaults to using the current class' name or - the `template_name` attribute + the `template_name` attribute. """ + if template_name: return template_name @@ -64,6 +75,7 @@ class View(object): return re.sub('[A-Z]', repl, template_name)[1:] + def _get_context(self): context = {} for item in self.context_list: @@ -71,12 +83,16 @@ class View(object): context.update(item) return context + def render(self, encoding=None): - return Template(self.get_template(self.template_name), self).render(encoding=encoding) + template = Template(self.get_template(self.template_name), self) + return template.render(encoding=encoding) + def __contains__(self, needle): return needle in self.context or hasattr(self, needle) + def __getitem__(self, attr): val = self.get(attr, None) @@ -84,11 +100,36 @@ class View(object): raise KeyError("Key '%s' does not exist in View" % attr) return val + def __getattr__(self, attr): if attr == 'context': return self._get_context() raise AttributeError("Attribute '%s' does not exist in View" % attr) + def __str__(self): - return self.render() \ No newline at end of file + return self.render() + + + +def get_or_attr(context_list, name, default=None): + """Returns an attribute from given context.""" + + if not context_list: + return default + + for obj in context_list: + try: + return obj[name] + except KeyError: + pass + except: + try: + return getattr(obj, name) + except AttributeError: + pass + + return default + + -- cgit v1.2.1 From e501ec273df9af0e2fe3e9752b19d0d94cc3884e Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:06:53 -0400 Subject: docstrings --- pystache/template.py | 4 ++++ pystache/view.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pystache/template.py b/pystache/template.py index 4fbb02c..c1b703c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -58,6 +58,8 @@ class Template(object): def _compile_regexps(self): + """Compiles regular expressions, based on otag/ctag values.""" + tags = { 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag) @@ -196,6 +198,8 @@ class Template(object): def render(self, encoding=None): + """Returns rendered Template string.""" + template = self._render_sections(self.template, self.view) result = self._render_tags(template) diff --git a/pystache/view.py b/pystache/view.py index 0e4732f..f907e51 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -35,6 +35,7 @@ class View(object): def get(self, attr, default=None): + """Returns given attribute value from View.""" attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) @@ -45,6 +46,7 @@ class View(object): def get_template(self, template_name): + """Returns current Template.""" if not self.template: @@ -85,6 +87,8 @@ class View(object): def render(self, encoding=None): + """Returns rendered Template.""" + template = Template(self.get_template(self.template_name), self) return template.render(encoding=encoding) -- cgit v1.2.1 From 5cd2886c622cd88ef2f68ea189a70ae9af3d8637 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:20:05 -0400 Subject: additional docstrings --- pystache/core.py | 10 ++++++++++ pystache/template.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/pystache/core.py b/pystache/core.py index 3d1b0b8..a02d89b 100644 --- a/pystache/core.py +++ b/pystache/core.py @@ -1,13 +1,23 @@ # -*- coding: utf-8 -*- +""" +pystache.core +~~~~~~~~~~~~~ + +This module provides the main entrance point to Pystache. +""" + + from .template import Template from .view import View from .loader import Loader + __all__ = ['Template', 'View', 'Loader', 'render'] def render(template, context=None, **kwargs): + """Renders a template string against the given context.""" context = context and context.copy() or {} context.update(kwargs) diff --git a/pystache/template.py b/pystache/template.py index c1b703c..e5ee690 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- +""" +pystache.template +~~~~~~~~~~~~~~~~~ + +This module provides Pystache's Template class. +""" + import re import cgi import collections @@ -15,6 +22,7 @@ except ImportError: literal = unicode + class Modifiers(dict): """Dictionary with a decorator for assigning functions to keys.""" -- cgit v1.2.1 From 43d504b5dbdb043cdfe9a87ffb4b3b239c130ab8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:25:28 -0400 Subject: setup.py cleanup --- setup.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index ad9981f..2556527 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import os import sys @@ -8,29 +9,32 @@ try: except ImportError: from distutils.core import setup + def publish(): """Publish to Pypi""" - os.system("python setup.py sdist upload") + os.system('python setup.py sdist upload') -if sys.argv[-1] == "publish": +if 'publish' in sys.argv: publish() sys.exit() -setup(name='pystache', - version='0.3.1', - description='Mustache for Python', - long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), - author='Chris Wanstrath', - author_email='chris@ozmm.org', - url='http://github.com/defunkt/pystache', - packages=['pystache'], - license='MIT', - classifiers = ( + +setup( + name='pystache', + version='0.3.1', + description='Mustache for Python', + long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), + author='Chris Wanstrath', + author_email='chris@ozmm.org', + url='http://github.com/defunkt/pystache', + packages=['pystache'], + license='MIT', + classifiers = ( "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2.5", - "Programming Language :: Python :: 2.6", - ) - ) + "Programming Language :: Python :: 2.6" + ) +) -- cgit v1.2.1 From 200ab9c3731a6f9baf8709c107fae0a5984d6b28 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:26:48 -0400 Subject: pypi => the cheeseshop --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2556527..3e2c0c0 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,11 @@ except ImportError: def publish(): - """Publish to Pypi""" + """Publishes module to The Cheeseshop.""" + os.system('python setup.py sdist upload') + if 'publish' in sys.argv: publish() sys.exit() -- cgit v1.2.1 From 6886173ce0694478b31b4e7acdb0bf0368167db2 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 23 Aug 2011 21:27:28 -0400 Subject: consistent string types --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 3e2c0c0..2dbbff7 100644 --- a/setup.py +++ b/setup.py @@ -32,11 +32,11 @@ setup( packages=['pystache'], license='MIT', classifiers = ( - "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 2.5", - "Programming Language :: Python :: 2.6" + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6' ) ) -- cgit v1.2.1 From b2ae206d68961b8aef568ceaa3dd1274150ef23e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 31 Oct 2011 19:06:20 -0400 Subject: Don't use `is` to compare integers. --- pystache/template.py | 2 +- pystache/view.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 563d830..209a47a 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -135,7 +135,7 @@ class Template(object): raw = self.view.get(tag_name, '') # For methods with no return value - if not raw and raw is not 0: + if not raw and raw != 0: if tag_name == '.': raw = self.view.context_list[0] else: diff --git a/pystache/view.py b/pystache/view.py index 925998e..a4e73b2 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -80,7 +80,7 @@ class View(object): def __getitem__(self, attr): val = self.get(attr, None) - if not val and val is not 0: + if not val and val != 0: raise KeyError("Key '%s' does not exist in View" % attr) return val @@ -91,4 +91,4 @@ class View(object): raise AttributeError("Attribute '%s' does not exist in View" % attr) def __str__(self): - return self.render() \ No newline at end of file + return self.render() -- cgit v1.2.1 From a09d5205803545accdef38bbdbc6c7c131114f90 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 1 Dec 2011 04:22:06 -0800 Subject: Added a code comment that collections.Callable is not available until Python 2.6. --- pystache/template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pystache/template.py b/pystache/template.py index 4197293..5643f46 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -15,6 +15,7 @@ except ImportError: try: + # The collections.Callable class is not available until Python 2.6. import collections.Callable def check_callable(it): return isinstance(it, collections.Callable) -- cgit v1.2.1 From d8067382eb2eda3ac212707e86b2bd252ea3ee32 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 1 Dec 2011 18:10:17 -0800 Subject: Loading of partials now respects the template extension configured on the view: * Template._render_partial() now passes self.view.template_extension to Loader.load_template(). * The test case test_examples.test_template_partial_extension now passes. * Corrected the expectation of the test case test_simple.test_template_partial_extension. It passes under the change to Template._render_partial(). * Removed trailing white space in test_simple.py. --- pystache/template.py | 2 +- tests/test_simple.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 5643f46..d9996a1 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -160,7 +160,7 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding) + markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding, extension=self.view.template_extension) template = Template(markup, self.view) return template.render() diff --git a/tests/test_simple.py b/tests/test_simple.py index 79e7a57..c01001f 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -7,14 +7,14 @@ from examples.template_partial import TemplatePartial from examples.simple import Simple class TestSimple(unittest.TestCase): - + def test_simple_render(self): self.assertEqual('herp', pystache.Template('{{derp}}', {'derp': 'herp'}).render()) - + def test_nested_context(self): view = NestedContext() self.assertEquals(pystache.Template('{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}', view).render(), "one and foo and two") - + def test_looping_and_negation_context(self): view = ComplexView() self.assertEquals(pystache.Template('{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}', view).render(), "Colors: red Colors: green Colors: blue ") @@ -22,28 +22,29 @@ class TestSimple(unittest.TestCase): def test_empty_context(self): view = ComplexView() self.assertEquals(pystache.Template('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me") - + def test_callables(self): view = Lambdas() self.assertEquals(pystache.Template('{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}', view).render(), 'bar != bar. oh, it does!') - + def test_rendering_partial(self): view = TemplatePartial() self.assertEquals(pystache.Template('{{>inner_partial}}', view).render(), 'Again, Welcome!') - + self.assertEquals(pystache.Template('{{#looping}}{{>inner_partial}} {{/looping}}', view).render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''') - + def test_non_existent_value_renders_blank(self): view = Simple() - + self.assertEquals(pystache.Template('{{not_set}} {{blank}}', view).render(), ' ') - - + + def test_template_partial_extension(self): view = TemplatePartial() view.template_extension = 'txt' self.assertEquals(view.render(), """Welcome ------- -Again, Welcome! +## Again, Welcome! ## + """) -- cgit v1.2.1 From 5f1c034defeeaf94c139cfa883e3337c78db5ea6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 1 Dec 2011 18:28:31 -0800 Subject: Added View.load_template() to eliminate some cut-and-paste. * Template._render_partial() now calls View.load_template(). * View.get_template() now calls View.load_template(). * Added test case test_view.py to test View.load_template(). * Removed trailing white space from view.py. * Removed trailing white space from test_view.py. --- pystache/template.py | 3 +-- pystache/view.py | 18 +++++++++++------- tests/test_view.py | 16 ++++++++++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d9996a1..2581ebb 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -159,8 +159,7 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): - from pystache import Loader - markup = Loader().load_template(template_name, self.view.template_path, encoding=self.view.template_encoding, extension=self.view.template_extension) + markup = self.view.load_template(template_name) template = Template(markup, self.view) return template.render() diff --git a/pystache/view.py b/pystache/view.py index 925998e..68cb9b6 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -20,33 +20,37 @@ def get_or_attr(context_list, name, default=None): return default class View(object): - + template_name = None template_path = None template = None template_encoding = None template_extension = 'mustache' - + def __init__(self, template=None, context=None, **kwargs): self.template = template context = context or {} context.update(**kwargs) self.context_list = [context] - + def get(self, attr, default=None): attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: return attr() else: return attr - + + def load_template(self, template_name): + from pystache import Loader + return Loader().load_template(template_name, self.template_path, + encoding=self.template_encoding, extension=self.template_extension) + def get_template(self, template_name): if not self.template: - from pystache import Loader template_name = self._get_template_name(template_name) - self.template = Loader().load_template(template_name, self.template_path, encoding=self.template_encoding, extension=self.template_extension) - + self.template = self.load_template(template_name) + return self.template def _get_template_name(self, template_name=None): diff --git a/tests/test_view.py b/tests/test_view.py index a3464df..3e1c968 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -22,6 +22,14 @@ class TestView(unittest.TestCase): view = Simple(thing='world') self.assertEquals(view.render(), "Hi world!") + def test_load_template(self): + """ + Test View.load_template(). + + """ + template = Simple().load_template("escaped") + self.assertEquals(template, "

{{title}}

") + def test_template_load_from_multiple_path(self): path = Simple.template_path Simple.template_path = ('examples/nowhere','examples',) @@ -57,7 +65,7 @@ class TestView(unittest.TestCase): self.assertEquals(view.render(), "Hi chris!") def test_complex(self): - self.assertEquals(ComplexView().render(), + self.assertEquals(ComplexView().render(), """

Colors

""") def test_higher_order_replace(self): @@ -74,12 +82,12 @@ class TestView(unittest.TestCase): view = Lambdas() view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') - + def test_partials_with_lambda(self): view = Lambdas() view.template = '{{>partial_with_lambda}}' self.assertEquals(view.render(), 'nopqrstuvwxyz') - + def test_hierarchical_partials_with_lambdas(self): view = Lambdas() view.template = '{{>partial_with_partial_and_lambda}}' @@ -95,7 +103,7 @@ class TestView(unittest.TestCase): parent.children = [Thing()] view = Simple(context={'parent': parent}) view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" - + self.assertEquals(view.render(), 'derp') def test_context_returns_a_flattened_dict(self): -- cgit v1.2.1 From 9923bef5008dd901f90527f18e1ac7d0df30a890 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Thu, 8 Dec 2011 16:41:33 +0000 Subject: Adding failing tests for #44 --- tests/test_pystache.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index c04489b..cb32706 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -75,5 +75,17 @@ class TestPystache(unittest.TestCase): ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") + def test_tag_in_value(self): + template = '{{test}}' + context = { 'test': '{{hello}}' } + ret = pystache.render(template, context) + self.assertEquals(ret, '{{hello}}') + + def test_section_in_value(self): + template = '{{test}}' + context = { 'test': '{{#hello}}' } + ret = pystache.render(template, context) + self.assertEquals(ret, '{{#hello}}') + if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 3e453f8207ba62dec63b106f381340001b11e74a Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Thu, 8 Dec 2011 17:38:59 +0000 Subject: Chewing through template rather than recursively replacing --- pystache/template.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 563d830..2ffd33f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -103,6 +103,8 @@ class Template(object): return template def _render_tags(self, template): + output = '' + while True: match = self.tag_re.search(template) if match is None: @@ -112,9 +114,11 @@ class Template(object): tag_name = tag_name.strip() func = self.modifiers[tag_type] replacement = func(self, tag_name) - template = template.replace(tag, replacement) - - return template + output = output + template[0:match.start()] + replacement + template = template[match.end():] + + output = output + template + return output def _render_dictionary(self, template, context): self.view.context_list.insert(0, context) -- cgit v1.2.1 From 1609163fa0c1fe09f7ece8d87be97f730883de06 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Thu, 8 Dec 2011 18:17:05 +0000 Subject: Adding failing test showing callable output being treated as template code --- tests/test_pystache.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index cb32706..1e44c8c 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -81,6 +81,14 @@ class TestPystache(unittest.TestCase): ret = pystache.render(template, context) self.assertEquals(ret, '{{hello}}') + def test_tag_in_labda_output(self): + template = '{{#test}}Blah{{/test}}' + context = { + 'test': lambda x: '{{hello}}' + } + ret = pystache.render(template, context) + self.assertEquals(ret, '{{hello}}') + def test_section_in_value(self): template = '{{test}}' context = { 'test': '{{#hello}}' } -- cgit v1.2.1 From a936076fd5c06f49c8a7307060861807d8245ffe Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Fri, 9 Dec 2011 12:22:53 +0000 Subject: Preventing section content being processed as a template. Fixes #46 --- pystache/template.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 2ffd33f..a48723b 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -68,7 +68,8 @@ class Template(object): tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) - def _render_sections(self, template, view): + def _render(self, template, view): + output = '' while True: match = self.section_re.search(template) if match is None: @@ -98,9 +99,15 @@ class Template(object): elif (not it and section[2] == '^') or (it and section[2] != '^'): replacer = self._render_dictionary(inner, it) - template = literal(template.replace(section, replacer)) + # Render template prior to section too + output = output + self._render_tags(template[0:match.start()]) + replacer + + template = template[match.end():] - return template + # Render remainder + output = output + self._render_tags(template) + + return output def _render_tags(self, template): output = '' @@ -172,8 +179,8 @@ class Template(object): return literal(self.view.get(tag_name, '')) def render(self, encoding=None): - template = self._render_sections(self.template, self.view) - result = self._render_tags(template) + result = self._render(self.template, self.view) + #result = self._render_tags(template) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From c49c0843ffe5c5918036072036e5defca3322349 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 12:48:26 -0800 Subject: Added code comments re: the spec's whitespace requirements. --- pystache/template.py | 2 ++ tests/test_pystache.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 4aa0710..486c7b2 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -72,6 +72,8 @@ class Template(object): 'ctag': re.escape(self.ctag) } + # The section contents include white space to comply with the spec's + # requirement that sections not alter surrounding whitespace. section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s" self.section_re = re.compile(section % tags, re.M|re.S) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index 16d457f..867f6f5 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -68,14 +68,15 @@ class TestPystache(unittest.TestCase): context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") - + def test_implicit_iterator(self): template = """
    {{#users}}
  • {{.}}
  • {{/users}}
""" context = { 'users': [ 'Chris', 'Tom','PJ' ] } ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") - def test_spacing(self): + # The spec says that sections should not alter surrounding whitespace. + def test_surrounding_whitepace_not_altered(self): template = "first{{#spacing}} second {{/spacing}}third" ret = pystache.render(template, {"spacing": True}) self.assertEquals(ret, "first second third") -- cgit v1.2.1 From 98d8b4032bfcbacd15b605f6062d7652d46ecdd9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 17:37:10 -0800 Subject: Removed spurious line added in previous commit. --- pystache/view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pystache/view.py b/pystache/view.py index f4c61d7..68ec37c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -64,7 +64,6 @@ class View(object): encoding=self.template_encoding, extension=self.template_extension) def get_template(self, template_name): - if not self.template: template_name = self._get_template_name(template_name) self.template = self.load_template(template_name) -- cgit v1.2.1 From f2c116c2b9e22904a4eb916f922a18847fe58d36 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 18:14:08 -0800 Subject: Stubbed out history notes for next release (need to add past changes). --- HISTORY.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 697e52e..224eeb0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,10 @@ History ======= +Next Release (version TBD) +-------------------------- +* Added some docstrings [kennethreitz]. + 0.4.0 (2011-01-12) ------------------ * Add support for nested contexts (within template and view) -- cgit v1.2.1 From 28379fe4231e21cd8d7e49ac5a146187ef12f8b8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 19:47:28 -0800 Subject: Added a note to the history file re: issue #27. --- HISTORY.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 224eeb0..50e0e79 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,9 @@ History Next Release (version TBD) -------------------------- -* Added some docstrings [kennethreitz]. +* Bugfix: Whitespace surrounding sections is no longer altered, in + accordance with the mustache spec. [heliodor] +* Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) ------------------ -- cgit v1.2.1 From 3c83ed332acad68481922817010149b210422be1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 20:20:14 -0800 Subject: Minor clean-ups of tests/test_loader.py. --- tests/test_loader.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index 42222ac..46c21cc 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,21 +1,23 @@ import unittest -import pystache -class TestLoader(unittest.TestCase): - +from pystache.loader import Loader + + +class LoaderTestCase(unittest.TestCase): + def test_template_is_loaded(self): - loader = pystache.Loader() + loader = Loader() template = loader.load_template('simple', 'examples') - + self.assertEqual(template, 'Hi {{thing}}!{{blank}}') - + def test_using_list_of_paths(self): - loader = pystache.Loader() + loader = Loader() template = loader.load_template('simple', ['doesnt_exist', 'examples']) - + self.assertEqual(template, 'Hi {{thing}}!{{blank}}') - + def test_non_existent_template_fails(self): - loader = pystache.Loader() - - self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist') \ No newline at end of file + loader = Loader() + + self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist') -- cgit v1.2.1 From a6559835c52e0a868bd4382e48b35e31e621ac63 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 20:27:13 -0800 Subject: Added a constructor to the Loader class. The constructor accepts search_dirs, template_encoding, and template_extension. --- pystache/loader.py | 18 ++++++++++++++---- tests/test_loader.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index d922258..2e61b92 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -10,9 +10,19 @@ import os class Loader(object): - template_extension = 'mustache' template_path = '.' - template_encoding = None + + def __init__(self, search_dirs=None, template_encoding=None, template_extension=None): + """ + Construct a template loader. + + """ + if template_extension is None: + template_extension = 'mustache' + + self.search_dirs = search_dirs + self.template_encoding = template_encoding + self.template_extension = template_extension def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): """ @@ -21,8 +31,8 @@ class Loader(object): Raises an IOError if the template cannot be found. """ - if None == template_dirs: - template_dirs = self.template_path + if template_dirs is None: + template_dirs = self.search_dirs or self.template_path if encoding is not None: self.template_encoding = encoding diff --git a/tests/test_loader.py b/tests/test_loader.py index 46c21cc..5cb9ff6 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -5,6 +5,21 @@ from pystache.loader import Loader class LoaderTestCase(unittest.TestCase): + def test_init(self): + """ + Test the __init__() constructor. + + """ + loader = Loader() + self.assertTrue(loader.search_dirs is None) + self.assertTrue(loader.template_encoding is None) + self.assertEquals(loader.template_extension, 'mustache') + + loader = Loader(search_dirs=['foo'], template_encoding='utf-8', template_extension='txt') + self.assertEquals(loader.search_dirs, ['foo']) + self.assertEquals(loader.template_encoding, 'utf-8') + self.assertEquals(loader.template_extension, 'txt') + def test_template_is_loaded(self): loader = Loader() template = loader.load_template('simple', 'examples') -- cgit v1.2.1 From 40a4b23c27e47b46b0655540b7362c7686604d56 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 20:41:40 -0800 Subject: The Loader.load_template() method now accepts only a template_name. --- pystache/loader.py | 21 +++++++-------------- pystache/view.py | 6 +++--- tests/test_loader.py | 12 ++++++------ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 2e61b92..9830544 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -12,33 +12,26 @@ class Loader(object): template_path = '.' - def __init__(self, search_dirs=None, template_encoding=None, template_extension=None): + def __init__(self, search_dirs=None, encoding=None, extension=None): """ Construct a template loader. """ - if template_extension is None: - template_extension = 'mustache' + if extension is None: + extension = 'mustache' self.search_dirs = search_dirs - self.template_encoding = template_encoding - self.template_extension = template_extension + self.template_encoding = encoding + self.template_extension = extension - def load_template(self, template_name, template_dirs=None, encoding=None, extension=None): + def load_template(self, template_name): """ Find and load the given template, and return it as a string. Raises an IOError if the template cannot be found. """ - if template_dirs is None: - template_dirs = self.search_dirs or self.template_path - - if encoding is not None: - self.template_encoding = encoding - - if extension is not None: - self.template_extension = extension + template_dirs = self.search_dirs or self.template_path file_name = template_name + '.' + self.template_extension diff --git a/pystache/view.py b/pystache/view.py index caa2f46..e277661 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -63,9 +63,9 @@ class View(object): return attr def load_template(self, template_name): - from pystache import Loader - return Loader().load_template(template_name, self.template_path, - encoding=self.template_encoding, extension=self.template_extension) + loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, + extension=self.template_extension) + return loader.load_template(template_name) def get_template(self, template_name): """ diff --git a/tests/test_loader.py b/tests/test_loader.py index 5cb9ff6..5f63058 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -15,24 +15,24 @@ class LoaderTestCase(unittest.TestCase): self.assertTrue(loader.template_encoding is None) self.assertEquals(loader.template_extension, 'mustache') - loader = Loader(search_dirs=['foo'], template_encoding='utf-8', template_extension='txt') + loader = Loader(search_dirs=['foo'], encoding='utf-8', extension='txt') self.assertEquals(loader.search_dirs, ['foo']) self.assertEquals(loader.template_encoding, 'utf-8') self.assertEquals(loader.template_extension, 'txt') def test_template_is_loaded(self): - loader = Loader() - template = loader.load_template('simple', 'examples') + loader = Loader(search_dirs='examples') + template = loader.load_template('simple') self.assertEqual(template, 'Hi {{thing}}!{{blank}}') def test_using_list_of_paths(self): - loader = Loader() - template = loader.load_template('simple', ['doesnt_exist', 'examples']) + loader = Loader(search_dirs=['doesnt_exist', 'examples']) + template = loader.load_template('simple') self.assertEqual(template, 'Hi {{thing}}!{{blank}}') def test_non_existent_template_fails(self): loader = Loader() - self.assertRaises(IOError, loader.load_template, 'simple', 'doesnt_exist') + self.assertRaises(IOError, loader.load_template, 'doesnt_exist') -- cgit v1.2.1 From 16aaa7f8a3d06e17ba044e8fa7dbb13d02dd8854 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 20:43:46 -0800 Subject: Switched to using os.curdir in the Loader class. --- pystache/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/loader.py b/pystache/loader.py index 9830544..8217fd5 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -10,7 +10,7 @@ import os class Loader(object): - template_path = '.' + template_path = os.curdir # i.e. "." def __init__(self, search_dirs=None, encoding=None, extension=None): """ -- cgit v1.2.1 From 89dd60590ec10a90d863260ab69b9f1fccbc414f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 20:53:22 -0800 Subject: Removed the Template.template_path instance attribute. Also simplified the Template.load_template() method. --- pystache/loader.py | 28 ++++++++++++++-------------- tests/test_loader.py | 3 ++- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 8217fd5..1233b2e 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -10,15 +10,23 @@ import os class Loader(object): - template_path = os.curdir # i.e. "." - def __init__(self, search_dirs=None, encoding=None, extension=None): """ Construct a template loader. + Arguments: + + search_dirs: the directories in which to search for templates. + Defaults to the current working directory. + """ if extension is None: extension = 'mustache' + if search_dirs is None: + search_dirs = os.curdir # i.e. "." + + if isinstance(search_dirs, basestring): + search_dirs = [search_dirs] self.search_dirs = search_dirs self.template_encoding = encoding @@ -31,24 +39,16 @@ class Loader(object): Raises an IOError if the template cannot be found. """ - template_dirs = self.search_dirs or self.template_path - + search_dirs = self.search_dirs file_name = template_name + '.' + self.template_extension - # Given a single directory, we'll load from it. - if isinstance(template_dirs, basestring): - file_path = os.path.join(template_dirs, file_name) - - return self._load_template_file(file_path) - - # Given a list of directories, we'll check each for our file. - for path in template_dirs: - file_path = os.path.join(path, file_name) + for dir_path in search_dirs: + file_path = os.path.join(dir_path, file_name) if os.path.exists(file_path): return self._load_template_file(file_path) # TODO: we should probably raise an exception of our own type. - raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(template_dirs),)) + raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) def _load_template_file(self, file_path): """ diff --git a/tests/test_loader.py b/tests/test_loader.py index 5f63058..7071d2b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,3 +1,4 @@ +import os import unittest from pystache.loader import Loader @@ -11,7 +12,7 @@ class LoaderTestCase(unittest.TestCase): """ loader = Loader() - self.assertTrue(loader.search_dirs is None) + self.assertEquals(loader.search_dirs, [os.curdir]) self.assertTrue(loader.template_encoding is None) self.assertEquals(loader.template_extension, 'mustache') -- cgit v1.2.1 From 64eb105689b5e58d2ccbb00ec50356bec12ccf95 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 21:11:18 -0800 Subject: The View class constructor now accepts a loader instance. --- pystache/view.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index e277661..b66bd10 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -42,7 +42,15 @@ class View(object): template_encoding = None template_extension = 'mustache' - def __init__(self, template=None, context=None, **kwargs): + template_loader = None + + def __init__(self, template=None, context=None, loader=None, **kwargs): + """ + Construct a View instance. + + """ + # TODO: add a unit test for passing a loader. + self.template_loader = loader self.template = template context = context or {} @@ -63,9 +71,14 @@ class View(object): return attr def load_template(self, template_name): - loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, - extension=self.template_extension) - return loader.load_template(template_name) + if self.template_loader is None: + # We delay setting the loader until now to allow users to set + # the template_extension attribute, etc. after View.__init__() + # has already been called. + self.template_loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, + extension=self.template_extension) + + return self.template_loader.load_template(template_name) def get_template(self, template_name): """ -- cgit v1.2.1 From a45277287867c99e0012bfad3925f2fcfdc14989 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 22:28:17 -0800 Subject: Removed the template_name parameter from View._get_template_name(), etc. --- pystache/view.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index b66bd10..1583da7 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -80,33 +80,31 @@ class View(object): return self.template_loader.load_template(template_name) - def get_template(self, template_name): + def get_template(self): """ Return the current template after setting it, if necessary. """ if not self.template: - template_name = self._get_template_name(template_name) + template_name = self._get_template_name() self.template = self.load_template(template_name) return self.template - # TODO: consider removing the template_name parameter and using - # self.template_name instead. - def _get_template_name(self, template_name=None): + def _get_template_name(self): """ Return the name of this Template instance. - If no template_name parameter is provided, this method returns the - class name modified as follows, for example: + If the template_name attribute is not set, then this method constructs + the template name from the class name as follows, for example: - TemplatePartial => template_partial + TemplatePartial => template_partial - Otherwise, it returns the given template_name. + Otherwise, this method returns the template_name. """ - if template_name: - return template_name + if self.template_name: + return self.template_name template_name = self.__class__.__name__ @@ -127,7 +125,7 @@ class View(object): Return the view rendered using the current context. """ - template = Template(self.get_template(self.template_name), self) + template = Template(self.get_template(), self) return template.render(encoding=encoding) def __contains__(self, needle): -- cgit v1.2.1 From ec120d99f72f9754436dabfeba4abeed8a121a10 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 22:52:44 -0800 Subject: The View constructor now accepts a load_template. Added a test case to load templates from a dictionary. --- pystache/view.py | 28 ++++++++++++++++------------ tests/test_view.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 1583da7..c9ee1c4 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -42,15 +42,17 @@ class View(object): template_encoding = None template_extension = 'mustache' - template_loader = None + # A function that accepts a single template_name parameter. + _load_template = None - def __init__(self, template=None, context=None, loader=None, **kwargs): + def __init__(self, template=None, context=None, load_template=None, **kwargs): """ Construct a View instance. """ - # TODO: add a unit test for passing a loader. - self.template_loader = loader + if load_template is not None: + self._load_template = load_template + self.template = template context = context or {} @@ -71,14 +73,16 @@ class View(object): return attr def load_template(self, template_name): - if self.template_loader is None: - # We delay setting the loader until now to allow users to set - # the template_extension attribute, etc. after View.__init__() - # has already been called. - self.template_loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, - extension=self.template_extension) - - return self.template_loader.load_template(template_name) + if self._load_template is None: + # We delay setting self._load_template until now (in the case + # that the user did not supply a load_template to the constructor) + # to let users set the template_extension attribute, etc. after + # View.__init__() has already been called. + loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, + extension=self.template_extension) + self._load_template = loader.load_template + + return self._load_template(template_name) def get_template(self): """ diff --git a/tests/test_view.py b/tests/test_view.py index 3e1c968..b1e0389 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -30,6 +30,19 @@ class TestView(unittest.TestCase): template = Simple().load_template("escaped") self.assertEquals(template, "

{{title}}

") + def test_custom_load_template(self): + """ + Test passing a custom load_template to View.__init__(). + + """ + partials_dict = {"partial": "Loaded from dictionary"} + load_template = lambda template_name: partials_dict[template_name] + + view = Simple(load_template=load_template) + + actual = view.load_template("partial") + self.assertEquals(actual, "Loaded from dictionary") + def test_template_load_from_multiple_path(self): path = Simple.template_path Simple.template_path = ('examples/nowhere','examples',) -- cgit v1.2.1 From ff48880ad2f72a58d38e3e0f628f88c52711638b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 23:01:25 -0800 Subject: Made sure that View.template does not get overwritten by the constructor. --- pystache/view.py | 3 ++- tests/test_view.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index c9ee1c4..f78fffb 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -53,7 +53,8 @@ class View(object): if load_template is not None: self._load_template = load_template - self.template = template + if template is not None: + self.template = template context = context or {} context.update(**kwargs) diff --git a/tests/test_view.py b/tests/test_view.py index b1e0389..a19d198 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -5,11 +5,26 @@ from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists +from pystache.view import View + class Thing(object): pass -class TestView(unittest.TestCase): + +class ViewTestCase(unittest.TestCase): + + def test_init(self): + """ + Test the constructor. + + """ + class TestView(View): + template = "foo" + + view = TestView() + self.assertEquals(view.template, "foo") + def test_basic(self): view = Simple("Hi {{thing}}!", { 'thing': 'world' }) self.assertEquals(view.render(), "Hi world!") -- cgit v1.2.1 From 38c51a865cebd367192df8d26d776ffd7627b1de Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 23:10:03 -0800 Subject: Updated history file for issue #47. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 50e0e79..dea9e3f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ Next Release (version TBD) -------------------------- * Bugfix: Whitespace surrounding sections is no longer altered, in accordance with the mustache spec. [heliodor] +* A custom template loader can now be passed to a View. [cjerdonek] * Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) -- cgit v1.2.1 From c76c6f9d2a3f98f32c2b791078559bdea59287ab Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 23:51:28 -0800 Subject: Added test cases to check that View.template_path is respected. --- tests/test_view.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_view.py b/tests/test_view.py index a19d198..f9560bd 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -58,6 +58,36 @@ class ViewTestCase(unittest.TestCase): actual = view.load_template("partial") self.assertEquals(actual, "Loaded from dictionary") + def test_template_path(self): + """ + Test that View.template_path is respected. + + """ + class Tagless(View): + pass + + view = Tagless() + self.assertRaises(IOError, view.render) + + view = Tagless() + view.template_path = "examples" + self.assertEquals(view.render(), "No tags...") + + def test_template_path_for_partials(self): + """ + Test that View.template_path is respected for partials. + + """ + class TestView(View): + template = "Partial: {{>tagless}}" + + view = TestView() + self.assertRaises(IOError, view.render) + + view = TestView() + view.template_path = "examples" + self.assertEquals(view.render(), "Partial: No tags...") + def test_template_load_from_multiple_path(self): path = Simple.template_path Simple.template_path = ('examples/nowhere','examples',) -- cgit v1.2.1 From 80fedec9eb3080b44e2430789085d1252706fca0 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Dec 2011 23:55:55 -0800 Subject: Adding file left out of previous commit: examples/tagless.mustache. --- examples/tagless.mustache | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/tagless.mustache diff --git a/examples/tagless.mustache b/examples/tagless.mustache new file mode 100644 index 0000000..ad4dd31 --- /dev/null +++ b/examples/tagless.mustache @@ -0,0 +1 @@ +No tags... \ No newline at end of file -- cgit v1.2.1 From 7b44843605ed0cf85545d6a39d5e79deb8396e60 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 16:54:06 -0800 Subject: Made spacing more consistent in setup.py. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6873732..20abedb 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,8 @@ setup(name='pystache', packages=['pystache'], license='MIT', entry_points = { - 'console_scripts': ['pystache=pystache.commands:main']}, + 'console_scripts': ['pystache=pystache.commands:main'], + }, classifiers = ( 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', -- cgit v1.2.1 From f8d1ac65452339f628345a9663794772348d6193 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 17:01:05 -0800 Subject: Put the code to create the package's long_description into a function. --- setup.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20abedb..efdb59f 100644 --- a/setup.py +++ b/setup.py @@ -19,14 +19,26 @@ def publish(): os.system('python setup.py sdist upload') +def make_long_description(): + """ + Return the long description for the package. + + """ + long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read() + + return long_description + + if sys.argv[-1] == 'publish': publish() sys.exit() +long_description = make_long_description() + setup(name='pystache', version='0.3.1', description='Mustache for Python', - long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), + long_description=long_description, author='Chris Wanstrath', author_email='chris@ozmm.org', url='http://github.com/defunkt/pystache', -- cgit v1.2.1 From 73f05e04e39990369001973fd0d934a79000a805 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 17:54:40 -0800 Subject: Minor clean-up like changes to the beginning of pystache/commands.py. --- pystache/commands.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index df208a3..5819406 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -1,7 +1,27 @@ -from pystache import Template +# coding: utf-8 + +""" +This module provides command-line access to pystache. + +Run this script using the -h option for command-line help. + +""" + +# TODO: allow option parsing to work in Python versions earlier than +# Python 2.7 (e.g. by using the optparse module). The argparse module +# isn't available until Python 2.7. import argparse import json -from loader import Loader + +# We use absolute imports here to allow use of this script from its +# location in source control (e.g. for development purposes). +# Otherwise, the following error occurs: +# +# ValueError: Attempted relative import in non-package +# +from pystache.loader import Loader +from pystache.template import Template + def main(): parser = argparse.ArgumentParser(description='Render a mustache template with the given context.') -- cgit v1.2.1 From d7dde61179494bf821d0f5df80421b8661b87013 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 19:22:56 -0800 Subject: Made commands.main() easier to unit test. --- pystache/commands.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 5819406..1b1846a 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -12,6 +12,7 @@ Run this script using the -h option for command-line help. # isn't available until Python 2.7. import argparse import json +import sys # We use absolute imports here to allow use of this script from its # location in source control (e.g. for development purposes). @@ -23,11 +24,13 @@ from pystache.loader import Loader from pystache.template import Template -def main(): +def main(sys_argv): + args = sys_argv[1:] + parser = argparse.ArgumentParser(description='Render a mustache template with the given context.') parser.add_argument('template', help='A filename or a template code.') parser.add_argument('context', help='A filename or a JSON string') - args = parser.parse_args() + args = parser.parse_args(args=args) if args.template.endswith('.mustache'): args.template = args.template[:-9] @@ -46,5 +49,5 @@ def main(): if __name__=='__main__': - main() + main(sys.argv) -- cgit v1.2.1 From b735548f64cbbd5a7f1444ff1584ccc639c955d3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 19:25:13 -0800 Subject: Added a working unit test of commands.main(). --- tests/test_commands.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_commands.py diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..f1817e7 --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,45 @@ +# coding: utf-8 + +""" +Unit tests of commands.py. + +""" + +import sys +import unittest + +from pystache.commands import main + + +ORIGINAL_STDOUT = sys.stdout + + +class MockStdout(object): + + def __init__(self): + self.output = "" + + def write(self, str): + self.output += str + + +class CommandsTestCase(unittest.TestCase): + + def setUp(self): + sys.stdout = MockStdout() + + def callScript(self, template, context): + argv = ['pystache', template, context] + main(argv) + return sys.stdout.output + + def testMainSimple(self): + """ + Test a simple command-line case. + + """ + actual = self.callScript("Hi {{thing}}", '{"thing": "world"}') + self.assertEquals(actual, u"Hi world\n") + + def tearDown(self): + sys.stdout = ORIGINAL_STDOUT -- cgit v1.2.1 From b07c1139a11162de9dd230c741b9617ec932c625 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 19:42:51 -0800 Subject: Made the command-line script work using Python 2.6. Switched to using optparse from argparse, which is only available starting in Python 2.7. --- pystache/commands.py | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 1b1846a..435f286 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -7,11 +7,10 @@ Run this script using the -h option for command-line help. """ -# TODO: allow option parsing to work in Python versions earlier than -# Python 2.7 (e.g. by using the optparse module). The argparse module -# isn't available until Python 2.7. -import argparse import json +# The optparse module is deprecated in Python 2.7 in favor of argparse. +# However, argparse is not available in Python 2.6 and earlier. +from optparse import OptionParser import sys # We use absolute imports here to allow use of this script from its @@ -24,26 +23,46 @@ from pystache.loader import Loader from pystache.template import Template -def main(sys_argv): +USAGE = """\ +%prog [-h] template context + +Render a mustache template with the given context. + +positional arguments: + template A filename or template string. + context A filename or JSON string.""" + + +def parse_args(sys_argv, usage): + """ + Return an OptionParser for the script. + + """ args = sys_argv[1:] - parser = argparse.ArgumentParser(description='Render a mustache template with the given context.') - parser.add_argument('template', help='A filename or a template code.') - parser.add_argument('context', help='A filename or a JSON string') - args = parser.parse_args(args=args) + parser = OptionParser(usage=usage) + options, args = parser.parse_args(args) + + template, context = args + + return template, context + + +def main(sys_argv): + template, context = parse_args(sys_argv, USAGE) - if args.template.endswith('.mustache'): - args.template = args.template[:-9] + if template.endswith('.mustache'): + template = template[:-9] try: - template = Loader().load_template(args.template) + template = Loader().load_template(template) except IOError: - template = args.template + pass try: - context = json.load(open(args.context)) + context = json.load(open(context)) except IOError: - context = json.loads(args.context) + context = json.loads(context) print(Template(template, context).render()) -- cgit v1.2.1 From 235df11beeab5e7f0cb4dadad623c14b156ae938 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 19:56:04 -0800 Subject: Updated the history file for issue #31. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index dea9e3f..6ddbfae 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,7 @@ Next Release (version TBD) * Bugfix: Whitespace surrounding sections is no longer altered, in accordance with the mustache spec. [heliodor] * A custom template loader can now be passed to a View. [cjerdonek] +* Added a command-line interface. [vrde, cjerdonek] * Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) -- cgit v1.2.1 From 699a9f75ea85ed7bc52e64445402b61db5a8334e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 20:25:45 -0800 Subject: Updated the history notes for issue #34. --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 6ddbfae..24d0be1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,8 @@ Next Release (version TBD) accordance with the mustache spec. [heliodor] * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] +* Bugfix: Fixed an issue that affected the rendering of zeroes when using + certain implementations of Python (i.e. PyPy). [alex] * Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) -- cgit v1.2.1 From 2c09a99bad77b93a1065beacaecc6ba170db82a9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Dec 2011 20:28:16 -0800 Subject: Tweaked the code comments for issue #34. --- pystache/template.py | 2 +- pystache/view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index b293f08..2dea793 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -169,7 +169,7 @@ class Template(object): # # We use "==" rather than "is" to compare integers, as using "is" relies # on an implementation detail of CPython. The test about rendering - # zeroes fails on PyPy when using "is". + # zeroes failed while using PyPy when using "is". # See issue #34: https://github.com/defunkt/pystache/issues/34 if not raw and raw != 0: if tag_name == '.': diff --git a/pystache/view.py b/pystache/view.py index 8fafb71..3acc6c8 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -141,7 +141,7 @@ class View(object): # We use "==" rather than "is" to compare integers, as using "is" relies # on an implementation detail of CPython. The test about rendering - # zeroes fails on PyPy when using "is". + # zeroes failed while using PyPy when using "is". # See issue #34: https://github.com/defunkt/pystache/issues/34 if not val and val != 0: raise KeyError("Key '%s' does not exist in View" % attr) -- cgit v1.2.1 From 220b3cd86b535d9b63f8ae808c396902456b48aa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 01:46:42 -0800 Subject: Fixed issue #50: "Template(foo='bar') raises an exception" The commit also adds a unit test for this case. --- pystache/template.py | 3 +++ tests/test_template.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/test_template.py diff --git a/pystache/template.py b/pystache/template.py index 2dea793..61391a7 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -66,6 +66,9 @@ class Template(object): self.template = template + if context is None: + context = {} + if kwargs: context.update(kwargs) diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..4166056 --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,22 @@ +# coding: utf-8 + +""" +Unit tests of template.py. + +""" + +import unittest + +from pystache.template import Template + + +class TemplateTestCase(unittest.TestCase): + + def test_init__kwargs_with_no_context(self): + """ + Test passing **kwargs with no context. + + """ + # This test checks that the following line raises no exception. + template = Template(foo="bar") + -- cgit v1.2.1 From 8aa126e320acd669b9538d85a2c487e0bd82c349 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 01:55:50 -0800 Subject: Updated history notes for issue #50. --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 24d0be1..a5c922f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,8 @@ History Next Release (version TBD) -------------------------- +* Bugfix: Passing **kwargs to Template.__init__() with no context + raised an exception. [cjerdonek] * Bugfix: Whitespace surrounding sections is no longer altered, in accordance with the mustache spec. [heliodor] * A custom template loader can now be passed to a View. [cjerdonek] -- cgit v1.2.1 From 78bb39e3ed4317008e2f2cc8f650c7e8f42ca5df Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 02:17:04 -0800 Subject: Fixed issue #51: "Passing **kwargs to Template.__init__() modifies the context" Also added a test case. --- pystache/template.py | 13 ++++++++++--- tests/test_template.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 61391a7..d46ba87 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,15 +62,22 @@ class Template(object): modifiers = Modifiers() def __init__(self, template=None, context=None, **kwargs): + """ + The **kwargs arguments are only supported if the context is + a dictionary (i.e. not a View). + + """ from .view import View self.template = template if context is None: context = {} - - if kwargs: - context.update(kwargs) + elif not isinstance(context, View): + # Views do not support copy() or update(). + context = context.copy() + if kwargs: + context.update(kwargs) self.view = context if isinstance(context, View) else View(context=context) self._compile_regexps() diff --git a/tests/test_template.py b/tests/test_template.py index 4166056..230a90a 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -12,6 +12,8 @@ from pystache.template import Template class TemplateTestCase(unittest.TestCase): + """Test the Template class.""" + def test_init__kwargs_with_no_context(self): """ Test passing **kwargs with no context. @@ -20,3 +22,12 @@ class TemplateTestCase(unittest.TestCase): # This test checks that the following line raises no exception. template = Template(foo="bar") + def test_init__kwargs_does_not_modify_context(self): + """ + Test that passing **kwargs does not modify the passed context. + + """ + context = {} + template = Template(context=context, foo="bar") + self.assertEquals(context, {}) + -- cgit v1.2.1 From 9029b4baff0ca89be1b9c48f505ad047932726c1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 02:19:47 -0800 Subject: Simplified pystache.render(): now shares code with Template.__init__(). --- pystache/init.py | 5 +---- pystache/template.py | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pystache/init.py b/pystache/init.py index c1eedf1..c821aed 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -18,7 +18,4 @@ def render(template, context=None, **kwargs): Return the given template string rendered using the given context. """ - context = context and context.copy() or {} - context.update(kwargs) - - return Template(template, context).render() + return Template(template, context, **kwargs).render() diff --git a/pystache/template.py b/pystache/template.py index d46ba87..d20051a 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -73,7 +73,8 @@ class Template(object): if context is None: context = {} - elif not isinstance(context, View): + + if not isinstance(context, View): # Views do not support copy() or update(). context = context.copy() if kwargs: -- cgit v1.2.1 From abeb2b2897219e5889182d12a892a7945f068a01 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 02:22:07 -0800 Subject: Updated the history notes for issue #51. --- HISTORY.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a5c922f..f1bec2a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,8 +3,9 @@ History Next Release (version TBD) -------------------------- -* Bugfix: Passing **kwargs to Template.__init__() with no context - raised an exception. [cjerdonek] +* Bugfix: Passing **kwargs to Template() modified the context. [cjerdonek] +* Bugfix: Passing **kwargs to Template() with no context raised an + exception. [cjerdonek] * Bugfix: Whitespace surrounding sections is no longer altered, in accordance with the mustache spec. [heliodor] * A custom template loader can now be passed to a View. [cjerdonek] -- cgit v1.2.1 From eb736a1c70edb087531661539bc2022ab9dea538 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 15 Dec 2011 19:40:56 -0800 Subject: Added a Context class with support for dictionaries (for issue #49). --- pystache/context.py | 61 ++++++++++++++++++++++++++++++++++++++++++ tests/test_context.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 pystache/context.py create mode 100644 tests/test_context.py diff --git a/pystache/context.py b/pystache/context.py new file mode 100644 index 0000000..cc8811b --- /dev/null +++ b/pystache/context.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +""" +Defines a Context class to represent mustache(5)'s notion of context. + +""" + + +class Context(object): + + """ + Encapsulates a queryable stack of zero or more dictionary-like objects. + + Instances of this class are intended to act as the context when + rendering mustache templates in accordance with mustache(5). + + """ + + # We reserve keyword arguments for future options (e.g. a "strict=True" + # option for enabling a strict mode). + def __init__(self, *obj): + """ + Construct an instance and initialize the stack. + + The variable argument list *obj are the objects with which to + populate the initial stack. Objects in the argument list are added + to the stack in order so that, in particular, items at the end of + the argument list are queried first when querying the stack. + + The objects should be dictionary-like in the following sense: + + (1) They can be dictionaries or objects. + (2) If they implement __getitem__, a KeyError should be raised + if __getitem__ is called on a missing key. + + """ + self.stack = list(obj) + + def get(self, key, default=None): + """ + Query the stack for the given key, and return the resulting value. + + Querying for a key queries objects in the stack in order from + last-added objects to first (last in, first out). + + Querying an item in the stack is done as follows: + + (1) The __getitem__ method is attempted first, if it exists. + + This method returns None if no item in the stack contains the key. + + """ + for obj in reversed(self.stack): + try: + return obj[key] + except KeyError: + pass + + return default + + diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..6a5dcc6 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,73 @@ +# coding: utf-8 + +""" +Unit tests of context.py. + +""" + +import unittest + +from pystache.context import Context + + +class ContextTestCase(unittest.TestCase): + + """ + Test the Context class. + + """ + + def test_init__no_elements(self): + """ + Check that passing nothing to __init__() raises no exception. + + """ + context = Context() + + def test_init__no_elements(self): + """ + Check that passing more than two items to __init__() raises no exception. + + """ + context = Context({}, {}, {}) + + def test_get__missing_key(self): + """ + Test getting a missing key. + + """ + context = Context() + self.assertTrue(context.get("foo") is None) + + def test_get__default(self): + """ + Test that get() respects the default value . + + """ + context = Context() + self.assertEquals(context.get("foo", "bar"), "bar") + + def test_get__key_present(self): + """ + Test get() with a key that is present. + + """ + context = Context({"foo": "bar"}) + self.assertEquals(context.get("foo"), "bar") + + def test_get__precedence(self): + """ + Test that get() respects the order of precedence (later items first). + + """ + context = Context({"foo": "bar"}, {"foo": "buzz"}) + self.assertEquals(context.get("foo"), "buzz") + + def test_get__fallback(self): + """ + Check that first-added stack items are queried on context misses. + + """ + context = Context({"fuzz": "buzz"}, {"foo": "bar"}) + self.assertEquals(context.get("fuzz"), "buzz") + -- cgit v1.2.1 From 4121be33463e5feef0df9226ea60a664db330e00 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:18:48 -0800 Subject: More progress on context module: defined and tested context._get_item(). --- pystache/context.py | 72 ++++++++++++++++++++++-- tests/test_context.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 215 insertions(+), 6 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index cc8811b..fd73174 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -5,6 +5,55 @@ Defines a Context class to represent mustache(5)'s notion of context. """ +# We use this private global variable as a return value to represent a key +# not being found on lookup. This lets us distinguish the case of a key's +# value being None with the case of a key not being found -- without having +# to rely on exceptions (e.g. KeyError) for flow control. +_NOT_FOUND = object() + + +# TODO: share code with template.check_callable(). +def _is_callable(obj): + return hasattr(obj, '__call__') + + +def _get_item(obj, key): + """ + Look up the given key in the given object, and return the value. + + The obj argument should satisfy the same conditions as those described + in Context.__init__'s() docstring. The behavior of this method is + undefined if obj is None. + + The rules for querying are the same as the rules described in + Context.get()'s docstring for a single item. + + Returns _NOT_FOUND if the key is not found. + + """ + if hasattr(obj, '__getitem__'): + # We do a membership test to avoid using exceptions for flow + # control. In addition, we call __contains__() explicitly as + # opposed to using the membership operator "in" to avoid + # triggering the following Python fallback behavior: + # + # For objects that don’t define __contains__(), the membership test + # first tries iteration via __iter__(), then the old sequence + # iteration protocol via __getitem__().... + # + # (from http://docs.python.org/reference/datamodel.html#object.__contains__ ) + if obj.__contains__(key): + return obj[key] + + elif hasattr(obj, key): + attr = getattr(obj, key) + if _is_callable(attr): + return attr() + + return attr + + return _NOT_FOUND + class Context(object): @@ -33,6 +82,19 @@ class Context(object): (2) If they implement __getitem__, a KeyError should be raised if __getitem__ is called on a missing key. + For efficiency, objects should implement __contains__() for more + efficient membership testing. From the Python documentation-- + + For objects that don’t define __contains__(), the membership test + first tries iteration via __iter__(), then the old sequence + iteration protocol via __getitem__().... + + (from http://docs.python.org/reference/datamodel.html#object.__contains__ ) + + Failing to implement __contains__() will cause undefined behavior. + on any key for which __getitem__() raises an exception [TODO: + also need to take __iter__() into account].... + """ self.stack = list(obj) @@ -51,10 +113,12 @@ class Context(object): """ for obj in reversed(self.stack): - try: - return obj[key] - except KeyError: - pass + val = _get_item(obj, key) + if val is _NOT_FOUND: + continue + # Otherwise, the key was found. + return val + # Otherwise, no item in the stack contained the key. return default diff --git a/tests/test_context.py b/tests/test_context.py index 6a5dcc6..f2864eb 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -7,10 +7,131 @@ Unit tests of context.py. import unittest +from pystache.context import _NOT_FOUND +from pystache.context import _get_item from pystache.context import Context -class ContextTestCase(unittest.TestCase): +class TestCase(unittest.TestCase): + + """A TestCase class with support for assertIs().""" + + # unittest.assertIs() is not available until Python 2.7: + # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone + def assertIs(self, first, second): + self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) + +class SimpleObject(object): + + """A sample class that does not define __getitem__().""" + + def __init__(self): + self.foo = "bar" + + def foo_callable(self): + return "called..." + + +class MappingObject(object): + + """A sample class that implements __getitem__() and __contains__().""" + + def __init__(self): + self._dict = {'foo': 'bar'} + self.fuzz = 'buzz' + + def __contains__(self, key): + return key in self._dict + + def __getitem__(self, key): + return self._dict[key] + + +class GetItemTestCase(TestCase): + + """Test context._get_item().""" + + def assertNotFound(self, obj, key): + self.assertIs(_get_item(obj, key), _NOT_FOUND) + + ### Case: obj is a dictionary. + + def test_dictionary__key_present(self): + """ + Test getting a key from a dictionary. + + """ + obj = {"foo": "bar"} + self.assertEquals(_get_item(obj, "foo"), "bar") + + def test_dictionary__key_missing(self): + """ + Test getting a missing key from a dictionary. + + """ + obj = {} + self.assertNotFound(obj, "missing") + + ### Case: obj does not implement __getitem__(). + + def test_object__attribute_present(self): + """ + Test getting an attribute from an object. + + """ + obj = SimpleObject() + self.assertEquals(_get_item(obj, "foo"), "bar") + + def test_object__attribute_missing(self): + """ + Test getting a missing attribute from an object. + + """ + obj = SimpleObject() + self.assertNotFound(obj, "missing") + + ### Case: obj implements __getitem__() (i.e. a "mapping object"). + + def test_mapping__key_present(self): + """ + Test getting a key from a mapping object. + + """ + obj = MappingObject() + self.assertEquals(_get_item(obj, "foo"), "bar") + + def test_mapping__key_missing(self): + """ + Test getting a missing key from a mapping object. + + """ + obj = MappingObject() + self.assertNotFound(obj, "missing") + + def test_mapping__get_attribute(self): + """ + Test getting an attribute from a mapping object. + + """ + obj = MappingObject() + self.assertEquals(obj.fuzz, "buzz") + self.assertNotFound(obj, "fuzz") + + def test_mapping_object__not_implementing_contains(self): + """ + Test querying a mapping object that doesn't define __contains__(). + + """ + class Sample(object): + + def __getitem__(self, key): + return "bar" + + obj = Sample() + self.assertRaises(AttributeError, _get_item, obj, "foo") + + +class ContextTestCase(TestCase): """ Test the Context class. @@ -24,7 +145,7 @@ class ContextTestCase(unittest.TestCase): """ context = Context() - def test_init__no_elements(self): + def test_init__many_elements(self): """ Check that passing more than two items to __init__() raises no exception. @@ -39,6 +160,14 @@ class ContextTestCase(unittest.TestCase): context = Context() self.assertTrue(context.get("foo") is None) + def test_get__dictionary_methods_not_queried(self): + """ + Test getting a missing key. + + """ + context = Context() + #self.assertEquals(context.get("keys"), 2) + def test_get__default(self): """ Test that get() respects the default value . @@ -71,3 +200,19 @@ class ContextTestCase(unittest.TestCase): context = Context({"fuzz": "buzz"}, {"foo": "bar"}) self.assertEquals(context.get("fuzz"), "buzz") + def test_get__object_attribute(self): + """ + Test that object attributes are queried. + + """ + context = Context(SimpleObject()) + self.assertEquals(context.get("foo"), "bar") + + def test_get__object_callable(self): + """ + Test that object callables are queried. + + """ + context = Context(SimpleObject()) + #self.assertEquals(context.get("foo_callable"), "called...") + -- cgit v1.2.1 From afb7f98de34a3d75ce930f1cdc3320d25b97bff7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:18:49 -0800 Subject: Minor docstring and code comment additions and improvements. --- pystache/context.py | 44 ++++++++++++++++++++++++-------------------- tests/test_context.py | 7 +++++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index fd73174..b0417dc 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -6,9 +6,9 @@ Defines a Context class to represent mustache(5)'s notion of context. """ # We use this private global variable as a return value to represent a key -# not being found on lookup. This lets us distinguish the case of a key's -# value being None with the case of a key not being found -- without having -# to rely on exceptions (e.g. KeyError) for flow control. +# not being found on lookup. This lets us distinguish between the case +# of a key's value being None with the case of a key not being found -- +# without having to rely on exceptions (e.g. KeyError) for flow control. _NOT_FOUND = object() @@ -19,27 +19,26 @@ def _is_callable(obj): def _get_item(obj, key): """ - Look up the given key in the given object, and return the value. + Return a key's value, or _NOT_FOUND if the key does not exist. - The obj argument should satisfy the same conditions as those described - in Context.__init__'s() docstring. The behavior of this method is - undefined if obj is None. + The obj argument should satisfy the same conditions as those + described for the obj arguments in Context.__init__'s() docstring. - The rules for querying are the same as the rules described in - Context.get()'s docstring for a single item. + The rules for looking up the value of a key are the same as the rules + described in Context.get()'s docstring for querying a single item. - Returns _NOT_FOUND if the key is not found. + The behavior of this function is undefined if obj is None. """ if hasattr(obj, '__getitem__'): - # We do a membership test to avoid using exceptions for flow - # control. In addition, we call __contains__() explicitly as - # opposed to using the membership operator "in" to avoid - # triggering the following Python fallback behavior: + # We do a membership test to avoid using exceptions for flow control + # (e.g. catching KeyError). In addition, we call __contains__() + # explicitly as opposed to using the membership operator "in" to + # avoid triggering the following Python fallback behavior: # - # For objects that don’t define __contains__(), the membership test + # "For objects that don’t define __contains__(), the membership test # first tries iteration via __iter__(), then the old sequence - # iteration protocol via __getitem__().... + # iteration protocol via __getitem__()...." # # (from http://docs.python.org/reference/datamodel.html#object.__contains__ ) if obj.__contains__(key): @@ -58,10 +57,15 @@ def _get_item(obj, key): class Context(object): """ - Encapsulates a queryable stack of zero or more dictionary-like objects. + Provides dictionary-like access to a stack of zero or more objects. - Instances of this class are intended to act as the context when - rendering mustache templates in accordance with mustache(5). + Instances of this class are meant to represent the rendering context + when rendering mustache templates in accordance with mustache(5). + + Querying the stack for the value of a key queries the objects in the + stack in order from last-added objects to first (last in, first out). + + See the docstrings of the methods of this class for more information. """ @@ -69,7 +73,7 @@ class Context(object): # option for enabling a strict mode). def __init__(self, *obj): """ - Construct an instance and initialize the stack. + Construct an instance, and initialize the stack. The variable argument list *obj are the objects with which to populate the initial stack. Objects in the argument list are added diff --git a/tests/test_context.py b/tests/test_context.py index f2864eb..d51f736 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -21,6 +21,7 @@ class TestCase(unittest.TestCase): def assertIs(self, first, second): self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) + class SimpleObject(object): """A sample class that does not define __getitem__().""" @@ -52,6 +53,10 @@ class GetItemTestCase(TestCase): """Test context._get_item().""" def assertNotFound(self, obj, key): + """ + Assert that a call to _get_item() returns _NOT_FOUND. + + """ self.assertIs(_get_item(obj, key), _NOT_FOUND) ### Case: obj is a dictionary. @@ -115,6 +120,8 @@ class GetItemTestCase(TestCase): """ obj = MappingObject() self.assertEquals(obj.fuzz, "buzz") + # The presence of __getitem__ causes obj.fuzz not to be checked, + # as desired. self.assertNotFound(obj, "fuzz") def test_mapping_object__not_implementing_contains(self): -- cgit v1.2.1 From 60f6802d55e27bc0a8bd7d3e074134e786febe77 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:18:49 -0800 Subject: More improvement tweaks. --- pystache/context.py | 21 +++++++++++---------- tests/test_context.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index b0417dc..a0a382f 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -57,12 +57,13 @@ def _get_item(obj, key): class Context(object): """ - Provides dictionary-like access to a stack of zero or more objects. + Provides dictionary-like access to a stack of zero or more items. Instances of this class are meant to represent the rendering context when rendering mustache templates in accordance with mustache(5). - Querying the stack for the value of a key queries the objects in the + Instances encapsulate a private stack of objects and dictionaries. + Querying the stack for the value of a key queries the items in the stack in order from last-added objects to first (last in, first out). See the docstrings of the methods of this class for more information. @@ -71,16 +72,16 @@ class Context(object): # We reserve keyword arguments for future options (e.g. a "strict=True" # option for enabling a strict mode). - def __init__(self, *obj): + def __init__(self, *items): """ - Construct an instance, and initialize the stack. + Construct an instance, and initialize the internal stack. - The variable argument list *obj are the objects with which to - populate the initial stack. Objects in the argument list are added - to the stack in order so that, in particular, items at the end of + The *items arguments are the items with which to populate the + initial stack. Items in the argument list are added to the + stack in order so that, in particular, items at the end of the argument list are queried first when querying the stack. - The objects should be dictionary-like in the following sense: + The items should satisfy the following: (1) They can be dictionaries or objects. (2) If they implement __getitem__, a KeyError should be raised @@ -100,7 +101,7 @@ class Context(object): also need to take __iter__() into account].... """ - self.stack = list(obj) + self._stack = list(items) def get(self, key, default=None): """ @@ -116,7 +117,7 @@ class Context(object): This method returns None if no item in the stack contains the key. """ - for obj in reversed(self.stack): + for obj in reversed(self._stack): val = _get_item(obj, key) if val is _NOT_FOUND: continue diff --git a/tests/test_context.py b/tests/test_context.py index d51f736..48324f2 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -77,6 +77,16 @@ class GetItemTestCase(TestCase): obj = {} self.assertNotFound(obj, "missing") + def test_dictionary__attributes_not_checked(self): + """ + Test that dictionary attributes are not checked. + + """ + obj = {} + attr_name = "keys" + self.assertEquals(getattr(obj, attr_name)(), []) + self.assertNotFound(obj, attr_name) + ### Case: obj does not implement __getitem__(). def test_object__attribute_present(self): @@ -119,10 +129,10 @@ class GetItemTestCase(TestCase): """ obj = MappingObject() - self.assertEquals(obj.fuzz, "buzz") - # The presence of __getitem__ causes obj.fuzz not to be checked, - # as desired. - self.assertNotFound(obj, "fuzz") + key = "fuzz" + self.assertEquals(getattr(obj, key), "buzz") + # As desired, __getitem__()'s presence causes obj.fuzz not to be checked. + self.assertNotFound(obj, key) def test_mapping_object__not_implementing_contains(self): """ -- cgit v1.2.1 From c8defb3fbb83e4b2ebea3d8492b1803acd011678 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:18:49 -0800 Subject: Fleshed out the docstring for Context.__init__(). --- pystache/context.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index a0a382f..6e0c09f 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -22,7 +22,8 @@ def _get_item(obj, key): Return a key's value, or _NOT_FOUND if the key does not exist. The obj argument should satisfy the same conditions as those - described for the obj arguments in Context.__init__'s() docstring. + described for the arguments passed to Context.__init__(). These + conditions are described in Context.__init__()'s docstring. The rules for looking up the value of a key are the same as the rules described in Context.get()'s docstring for querying a single item. @@ -59,7 +60,7 @@ class Context(object): """ Provides dictionary-like access to a stack of zero or more items. - Instances of this class are meant to represent the rendering context + Instances of this class are meant to act as the rendering context when rendering mustache templates in accordance with mustache(5). Instances encapsulate a private stack of objects and dictionaries. @@ -74,31 +75,29 @@ class Context(object): # option for enabling a strict mode). def __init__(self, *items): """ - Construct an instance, and initialize the internal stack. + Construct an instance, and initialize the private stack. The *items arguments are the items with which to populate the initial stack. Items in the argument list are added to the stack in order so that, in particular, items at the end of the argument list are queried first when querying the stack. - The items should satisfy the following: + Each item should satisfy the following condition: - (1) They can be dictionaries or objects. - (2) If they implement __getitem__, a KeyError should be raised - if __getitem__ is called on a missing key. + * If the item implements __getitem__(), it should also implement + __contains__(). Failure to implement __contains__() will cause + an AttributeError to be raised when the item is queried during + calls to self.get(). - For efficiency, objects should implement __contains__() for more - efficient membership testing. From the Python documentation-- + Python dictionaries, in particular, satisfy this condition. + An item satisfying this condition we informally call a "mapping + object" because it shares some characteristics of the Mapping + abstract base class (ABC) in Python's collections package: + http://docs.python.org/library/collections.html#collections-abstract-base-classes - For objects that don’t define __contains__(), the membership test - first tries iteration via __iter__(), then the old sequence - iteration protocol via __getitem__().... - - (from http://docs.python.org/reference/datamodel.html#object.__contains__ ) - - Failing to implement __contains__() will cause undefined behavior. - on any key for which __getitem__() raises an exception [TODO: - also need to take __iter__() into account].... + It is not necessary for an item to implement __getitem__(). + In particular, an item can be an ordinary object with no + mapping-like characteristics. """ self._stack = list(items) -- cgit v1.2.1 From e00931d447d01f3a024e53e302dfa5decca7f76d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:18:49 -0800 Subject: Finished fleshing out the initial docstrings for the context module. --- pystache/context.py | 26 ++++++++++++++++++-------- tests/test_context.py | 8 ++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index 6e0c09f..4c7d857 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -106,14 +106,24 @@ class Context(object): """ Query the stack for the given key, and return the resulting value. - Querying for a key queries objects in the stack in order from - last-added objects to first (last in, first out). - - Querying an item in the stack is done as follows: - - (1) The __getitem__ method is attempted first, if it exists. - - This method returns None if no item in the stack contains the key. + Querying for a key queries items in the stack in order from last- + added objects to first (last in, first out). The value returned + is the value of the key for the first item for which the item + contains the key. If the key is not found in any item in the + stack, then this method returns the default value. The default + value defaults to None. + + Querying an item in the stack is done in the following way: + + (1) If the item defines __getitem__() and the item contains the + key (i.e. __contains__() returns True), then the corresponding + value is returned. + (2) Otherwise, the method looks for an attribute with the same + name as the key. If such an attribute exists, the value of + this attribute is returned. If the attribute is callable, + however, the attribute is first called with no arguments. + (3) If there is no attribute with the same name as the key, then + the key is considered not found in the item. """ for obj in reversed(self._stack): diff --git a/tests/test_context.py b/tests/test_context.py index 48324f2..4c8929c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -105,6 +105,14 @@ class GetItemTestCase(TestCase): obj = SimpleObject() self.assertNotFound(obj, "missing") + def test_object__attribute_is_callable(self): + """ + Test getting a callable attribute from an object. + + """ + obj = SimpleObject() + self.assertEquals(_get_item(obj, "foo_callable"), "called...") + ### Case: obj implements __getitem__() (i.e. a "mapping object"). def test_mapping__key_present(self): -- cgit v1.2.1 From 55846ad76d8eb15da33355161800b408edabd93c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:27:14 -0800 Subject: Cleaned up and removed redundant unit tests of the context module. --- tests/test_context.py | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index 4c8929c..1e4b883 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -177,21 +177,21 @@ class ContextTestCase(TestCase): """ context = Context({}, {}, {}) - def test_get__missing_key(self): + def test_get__key_present(self): """ - Test getting a missing key. + Test getting a key. """ - context = Context() - self.assertTrue(context.get("foo") is None) + context = Context({"foo": "bar"}) + self.assertEquals(context.get("foo"), "bar") - def test_get__dictionary_methods_not_queried(self): + def test_get__key_missing(self): """ Test getting a missing key. """ context = Context() - #self.assertEquals(context.get("keys"), 2) + self.assertTrue(context.get("foo") is None) def test_get__default(self): """ @@ -201,14 +201,6 @@ class ContextTestCase(TestCase): context = Context() self.assertEquals(context.get("foo", "bar"), "bar") - def test_get__key_present(self): - """ - Test get() with a key that is present. - - """ - context = Context({"foo": "bar"}) - self.assertEquals(context.get("foo"), "bar") - def test_get__precedence(self): """ Test that get() respects the order of precedence (later items first). @@ -225,19 +217,3 @@ class ContextTestCase(TestCase): context = Context({"fuzz": "buzz"}, {"foo": "bar"}) self.assertEquals(context.get("fuzz"), "buzz") - def test_get__object_attribute(self): - """ - Test that object attributes are queried. - - """ - context = Context(SimpleObject()) - self.assertEquals(context.get("foo"), "bar") - - def test_get__object_callable(self): - """ - Test that object callables are queried. - - """ - context = Context(SimpleObject()) - #self.assertEquals(context.get("foo_callable"), "called...") - -- cgit v1.2.1 From a072481143efc5cdf1747e19a06c60a60ceb9aa6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:44:04 -0800 Subject: Implemented Context.push(). --- pystache/context.py | 3 +++ tests/test_context.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pystache/context.py b/pystache/context.py index 4c7d857..fe61fcc 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -136,4 +136,7 @@ class Context(object): return default + def push(self, item): + self._stack.append(item) + diff --git a/tests/test_context.py b/tests/test_context.py index 1e4b883..2e372b3 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -217,3 +217,15 @@ class ContextTestCase(TestCase): context = Context({"fuzz": "buzz"}, {"foo": "bar"}) self.assertEquals(context.get("fuzz"), "buzz") + def test_push(self): + """ + Test push(). + + """ + key = "foo" + context = Context({key: "bar"}) + self.assertEquals(context.get(key), "bar") + + context.push({key: "buzz"}) + self.assertEquals(context.get(key), "buzz") + -- cgit v1.2.1 From 7a7d886e5d56c189a74e4f9e17d44fe8974e5748 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Dec 2011 18:47:14 -0800 Subject: Implemented Context.pop(). --- pystache/context.py | 2 ++ tests/test_context.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pystache/context.py b/pystache/context.py index fe61fcc..1f764e7 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -139,4 +139,6 @@ class Context(object): def push(self, item): self._stack.append(item) + def pop(self): + return self._stack.pop() diff --git a/tests/test_context.py b/tests/test_context.py index 2e372b3..6951180 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -229,3 +229,16 @@ class ContextTestCase(TestCase): context.push({key: "buzz"}) self.assertEquals(context.get(key), "buzz") + def test_pop(self): + """ + Test pop(). + + """ + key = "foo" + context = Context({key: "bar"}, {key: "buzz"}) + self.assertEquals(context.get(key), "buzz") + + item = context.pop() + self.assertEquals(item, {"foo": "buzz"}) + self.assertEquals(context.get(key), "bar") + -- cgit v1.2.1 From 33b265779cec681ad8a7e07fd8454d72b76e04dd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 01:08:28 -0800 Subject: Implemented (and tested) Context.top() and Context.copy(). --- pystache/context.py | 21 +++++++++++++++++++++ tests/test_context.py | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/pystache/context.py b/pystache/context.py index 1f764e7..36140c7 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -137,8 +137,29 @@ class Context(object): return default def push(self, item): + """ + Push an item onto the stack. + + """ self._stack.append(item) def pop(self): + """ + Pop an item off of the stack, and return it. + + """ return self._stack.pop() + def top(self): + """ + Return the item last added to the stack. + + """ + return self._stack[-1] + + def copy(self): + """ + Return a copy of this instance. + + """ + return Context(*self._stack) diff --git a/tests/test_context.py b/tests/test_context.py index 6951180..dd25a5e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -242,3 +242,27 @@ class ContextTestCase(TestCase): self.assertEquals(item, {"foo": "buzz"}) self.assertEquals(context.get(key), "bar") + def test_top(self): + key = "foo" + context = Context({key: "bar"}, {key: "buzz"}) + self.assertEquals(context.get(key), "buzz") + + top = context.top() + self.assertEquals(top, {"foo": "buzz"}) + # Make sure calling top() didn't remove the item from the stack. + self.assertEquals(context.get(key), "buzz") + + def test_copy(self): + key = "foo" + original = Context({key: "bar"}, {key: "buzz"}) + self.assertEquals(original.get(key), "buzz") + + new = original.copy() + # Confirm that the copy behaves the same. + self.assertEquals(new.get(key), "buzz") + # Change the copy, and confirm it is changed. + new.pop() + self.assertEquals(new.get(key), "bar") + # Confirm the original is unchanged. + self.assertEquals(original.get(key), "buzz") + -- cgit v1.2.1 From 236d7d46fb059aaba3c5669c76ab2fad7aef3149 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 04:31:13 -0800 Subject: Removed View.__getitem__(). Note that the removed logic around issue #34 remains elsewhere in the code in the Template._render_tag() method. --- examples/template_partial.py | 4 ++-- pystache/view.py | 11 ----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/examples/template_partial.py b/examples/template_partial.py index 651cbe5..06ebe38 100644 --- a/examples/template_partial.py +++ b/examples/template_partial.py @@ -11,6 +11,6 @@ class TemplatePartial(pystache.View): def looping(self): return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] - + def thing(self): - return self['prop'] \ No newline at end of file + return self.get('prop') \ No newline at end of file diff --git a/pystache/view.py b/pystache/view.py index 3acc6c8..4eca3bf 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -136,17 +136,6 @@ class View(object): def __contains__(self, needle): return needle in self.context or hasattr(self, needle) - def __getitem__(self, attr): - val = self.get(attr, None) - - # We use "==" rather than "is" to compare integers, as using "is" relies - # on an implementation detail of CPython. The test about rendering - # zeroes failed while using PyPy when using "is". - # See issue #34: https://github.com/defunkt/pystache/issues/34 - if not val and val != 0: - raise KeyError("Key '%s' does not exist in View" % attr) - return val - def __getattr__(self, attr): if attr == 'context': return self._get_context() -- cgit v1.2.1 From a68ddb39b28f0e2d27ffbff2e11e1282f6bb69b7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 05:09:17 -0800 Subject: Simplified the logic in Template.__init__() slightly. --- pystache/template.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d20051a..c79c66f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -77,10 +77,12 @@ class Template(object): if not isinstance(context, View): # Views do not support copy() or update(). context = context.copy() - if kwargs: - context.update(kwargs) + view = View(context=context, **kwargs) + else: + view = context + + self.view = view - self.view = context if isinstance(context, View) else View(context=context) self._compile_regexps() def _compile_regexps(self): -- cgit v1.2.1 From c689ce939eb90125ffaba44cccc24ddc111c006c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 05:20:15 -0800 Subject: Bug fix: View(context=context, **kwargs) could modify the passed context. Also added a test case. --- pystache/view.py | 3 +++ tests/test_view.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/pystache/view.py b/pystache/view.py index 4eca3bf..a62fd84 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -50,6 +50,9 @@ class View(object): Construct a View instance. """ + if context is None: + context = {} + if load_template is not None: self._load_template = load_template diff --git a/tests/test_view.py b/tests/test_view.py index f9560bd..a70672d 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -25,6 +25,15 @@ class ViewTestCase(unittest.TestCase): view = TestView() self.assertEquals(view.template, "foo") + def test_init__kwargs_does_not_modify_context(self): + """ + Test that passing **kwargs does not modify the passed context. + + """ + context = {"foo": "bar"} + view = View(context=context, fuzz="buzz") + self.assertEquals(context, {"foo": "bar"}) + def test_basic(self): view = Simple("Hi {{thing}}!", { 'thing': 'world' }) self.assertEquals(view.render(), "Hi world!") -- cgit v1.2.1 From bcd4afe9ef28997853e53e2d29e292cc29dffa2c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 05:27:11 -0800 Subject: View class now uses new Context class. --- pystache/template.py | 6 ++--- pystache/view.py | 64 ++++++++-------------------------------------------- tests/test_view.py | 6 ----- 3 files changed, 12 insertions(+), 64 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index c79c66f..0bb2b53 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -158,12 +158,12 @@ class Template(object): return template def _render_dictionary(self, template, context): - self.view.context_list.insert(0, context) + self.view.context.push(context) template = Template(template, self.view) out = template.render() - self.view.context_list.pop(0) + self.view.context.pop() return out @@ -186,7 +186,7 @@ class Template(object): # See issue #34: https://github.com/defunkt/pystache/issues/34 if not raw and raw != 0: if tag_name == '.': - raw = self.view.context_list[0] + raw = self.view.context.top() else: return '' diff --git a/pystache/view.py b/pystache/view.py index a62fd84..946ee25 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -8,32 +8,11 @@ This module provides a View class. import re from types import UnboundMethodType +from .context import Context from .loader import Loader from .template import Template -def get_or_attr(context_list, name, default=None): - """ - Find and return an attribute from the given context. - - """ - if not context_list: - return default - - for obj in context_list: - try: - return obj[name] - except KeyError: - pass - except: - try: - return getattr(obj, name) - except AttributeError: - pass - - return default - - class View(object): template_name = None @@ -50,31 +29,19 @@ class View(object): Construct a View instance. """ - if context is None: - context = {} - if load_template is not None: self._load_template = load_template if template is not None: self.template = template - context = context or {} - context.update(**kwargs) - - self.context_list = [context] + _context = Context(self) + if context: + _context.push(context) + if kwargs: + _context.push(kwargs) - def get(self, attr, default=None): - """ - Return the value for the given attribute. - - """ - attr = get_or_attr(self.context_list, attr, getattr(self, attr, default)) - - if hasattr(attr, '__call__') and type(attr) is UnboundMethodType: - return attr() - else: - return attr + self.context = _context def load_template(self, template_name): if self._load_template is None: @@ -121,13 +88,6 @@ class View(object): return re.sub('[A-Z]', repl, template_name)[1:] - def _get_context(self): - context = {} - for item in self.context_list: - if hasattr(item, 'keys') and hasattr(item, '__getitem__'): - context.update(item) - return context - def render(self, encoding=None): """ Return the view rendered using the current context. @@ -136,14 +96,8 @@ class View(object): template = Template(self.get_template(), self) return template.render(encoding=encoding) - def __contains__(self, needle): - return needle in self.context or hasattr(self, needle) - - def __getattr__(self, attr): - if attr == 'context': - return self._get_context() - - raise AttributeError("Attribute '%s' does not exist in View" % attr) + def get(self, key, default=None): + return self.context.get(key, default) def __str__(self): return self.render() diff --git a/tests/test_view.py b/tests/test_view.py index a70672d..b68da2b 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -173,12 +173,6 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), 'derp') - def test_context_returns_a_flattened_dict(self): - view = Simple() - view.context_list = [{'one':'1'}, {'two':'2'}, object()] - - self.assertEqual(view.context, {'one': '1', 'two': '2'}) - def test_inverted_lists(self): view = InvertedLists() self.assertEquals(view.render(), """one, two, three, empty list""") -- cgit v1.2.1 From 050ab67d59ce8c546e944d1aa376d0c27ff67d37 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 11:29:28 -0800 Subject: Completed issue #49: the Template class now manages the rendering context. --- pystache/template.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 0bb2b53..b2db363 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -9,6 +9,7 @@ import re import cgi import collections +from .context import Context from .loader import Loader @@ -63,24 +64,34 @@ class Template(object): def __init__(self, template=None, context=None, **kwargs): """ - The **kwargs arguments are only supported if the context is - a dictionary (i.e. not a View). + The context argument can be a dictionary, View, or Context instance. """ from .view import View - self.template = template - if context is None: context = {} - if not isinstance(context, View): - # Views do not support copy() or update(). + view = None + + if isinstance(context, View): + view = context + context = view.context.copy() + elif isinstance(context, Context): context = context.copy() - view = View(context=context, **kwargs) else: - view = context + # Otherwise, the context is a dictionary. + context = Context(context) + + if kwargs: + context.push(kwargs) + if view is None: + view = View() + + self.context = context + self.template = template + # The view attribute is used only for its load_template() method. self.view = view self._compile_regexps() @@ -113,7 +124,7 @@ class Template(object): section, section_name, inner = match.group(0, 1, 2) section_name = section_name.strip() - it = self.view.get(section_name, None) + it = self.context.get(section_name, None) replacer = '' # Callable @@ -158,12 +169,13 @@ class Template(object): return template def _render_dictionary(self, template, context): - self.view.context.push(context) + self.context.push(context) - template = Template(template, self.view) + template = Template(template, self.context) + template.view = self.view out = template.render() - self.view.context.pop() + self.context.pop() return out @@ -176,7 +188,7 @@ class Template(object): @modifiers.set(None) def _render_tag(self, tag_name): - raw = self.view.get(tag_name, '') + raw = self.context.get(tag_name, '') # For methods with no return value # @@ -186,7 +198,7 @@ class Template(object): # See issue #34: https://github.com/defunkt/pystache/issues/34 if not raw and raw != 0: if tag_name == '.': - raw = self.view.context.top() + raw = self.context.top() else: return '' @@ -199,7 +211,8 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.view.load_template(template_name) - template = Template(markup, self.view) + template = Template(markup, self.context) + template.view = self.view return template.render() @modifiers.set('=') @@ -220,7 +233,7 @@ class Template(object): Render a tag without escaping it. """ - return literal(self.view.get(tag_name, '')) + return literal(self.context.get(tag_name, '')) def render(self, encoding=None): """ -- cgit v1.2.1 From b2137d4f77bd706495ec468126687a3bac1cb856 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 11:31:10 -0800 Subject: Removed unused view parameter from Template._render_sections(). --- pystache/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index b2db363..010a015 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -116,7 +116,7 @@ class Template(object): tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" self.tag_re = re.compile(tag % tags) - def _render_sections(self, template, view): + def _render_sections(self, template): while True: match = self.section_re.search(template) if match is None: @@ -240,7 +240,7 @@ class Template(object): Return the template rendered using the current view context. """ - template = self._render_sections(self.template, self.view) + template = self._render_sections(self.template) result = self._render_tags(template) if encoding is not None: -- cgit v1.2.1 From 76dcc8ed0c8745c648f0508d61497945d182edaa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 12:10:40 -0800 Subject: View class now relies on the Loader class's default template extension. --- pystache/loader.py | 5 ++++- pystache/view.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 1233b2e..9e59774 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -8,6 +8,9 @@ This module provides a Loader class. import os +DEFAULT_EXTENSION = 'mustache' + + class Loader(object): def __init__(self, search_dirs=None, encoding=None, extension=None): @@ -21,7 +24,7 @@ class Loader(object): """ if extension is None: - extension = 'mustache' + extension = DEFAULT_EXTENSION if search_dirs is None: search_dirs = os.curdir # i.e. "." diff --git a/pystache/view.py b/pystache/view.py index 946ee25..840c8b2 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -19,7 +19,7 @@ class View(object): template_path = None template = None template_encoding = None - template_extension = 'mustache' + template_extension = None # A function that accepts a single template_name parameter. _load_template = None -- cgit v1.2.1 From ccfa60756833016faccae05eb7566cdd24e1423e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 12:23:32 -0800 Subject: Removed the Template class's view attribute. --- pystache/template.py | 28 +++++++++++++++++----------- pystache/view.py | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 010a015..a38add3 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,9 +62,16 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, context=None, **kwargs): + def __init__(self, template=None, context=None, load_template=None, **kwargs): """ - The context argument can be a dictionary, View, or Context instance. + + Arguments: + + context: a dictionary, View, or Context instance. + + load_template: a function that accepts a single template_name + parameter and returns a template as a string. Defaults to the + default Loader's load_template() method. """ from .view import View @@ -77,6 +84,7 @@ class Template(object): if isinstance(context, View): view = context context = view.context.copy() + load_template = view.load_template elif isinstance(context, Context): context = context.copy() else: @@ -86,13 +94,13 @@ class Template(object): if kwargs: context.push(kwargs) - if view is None: - view = View() + if load_template is None: + loader = Loader() + load_template = loader.load_template self.context = context + self.load_template = load_template self.template = template - # The view attribute is used only for its load_template() method. - self.view = view self._compile_regexps() @@ -171,8 +179,7 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - template = Template(template, self.context) - template.view = self.view + template = Template(template, self.context, self.load_template) out = template.render() self.context.pop() @@ -210,9 +217,8 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): - markup = self.view.load_template(template_name) - template = Template(markup, self.context) - template.view = self.view + markup = self.load_template(template_name) + template = Template(markup, self.context, self.load_template) return template.render() @modifiers.set('=') diff --git a/pystache/view.py b/pystache/view.py index 840c8b2..807bb55 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -93,7 +93,7 @@ class View(object): Return the view rendered using the current context. """ - template = Template(self.get_template(), self) + template = Template(self.get_template(), self.context, self.load_template) return template.render(encoding=encoding) def get(self, key, default=None): -- cgit v1.2.1 From cbe677ccc79216c1438ff3984730df73a5bf9c9a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Dec 2011 12:34:22 -0800 Subject: Addressed issue #48: "Template class should not depend on the View class" The template module no longer imports from the view module. --- pystache/template.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index a38add3..471b96e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -64,40 +64,33 @@ class Template(object): def __init__(self, template=None, context=None, load_template=None, **kwargs): """ + Construct a Template instance. Arguments: - context: a dictionary, View, or Context instance. + context: a dictionary, Context, or View instance. - load_template: a function that accepts a single template_name - parameter and returns a template as a string. Defaults to the - default Loader's load_template() method. + load_template: the function for loading partials. The function should + accept a single template_name parameter and return a template as + a string. Defaults to the default Loader's load_template() method. """ - from .view import View - if context is None: context = {} - view = None + if load_template is None: + loader = Loader() + load_template = loader.load_template + load_template = getattr(context, 'load_template', load_template) - if isinstance(context, View): - view = context - context = view.context.copy() - load_template = view.load_template - elif isinstance(context, Context): + if isinstance(context, Context): context = context.copy() else: - # Otherwise, the context is a dictionary. context = Context(context) if kwargs: context.push(kwargs) - if load_template is None: - loader = Loader() - load_template = loader.load_template - self.context = context self.load_template = load_template self.template = template @@ -243,7 +236,7 @@ class Template(object): def render(self, encoding=None): """ - Return the template rendered using the current view context. + Return the template rendered using the current context. """ template = self._render_sections(self.template) -- cgit v1.2.1 From 84f080421f3b244bc8427469f8fee1072e21954b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 12:28:17 -0800 Subject: Added some unit tests for Template.render(). --- tests/test_template.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_template.py b/tests/test_template.py index 230a90a..4cdf4ad 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -5,6 +5,7 @@ Unit tests of template.py. """ +import codecs import unittest from pystache.template import Template @@ -31,3 +32,25 @@ class TemplateTestCase(unittest.TestCase): template = Template(context=context, foo="bar") self.assertEquals(context, {}) + def test_render__unicode(self): + template = Template(u'foo') + actual = template.render() + self.assertTrue(isinstance(actual, unicode)) + self.assertEquals(actual, u'foo') + + def test_render__str(self): + template = Template('foo') + actual = template.render() + self.assertTrue(isinstance(actual, str)) + self.assertEquals(actual, 'foo') + + def test_render__context(self): + template = Template('Hi {{person}}', {'person': 'Mom'}) + self.assertEquals(template.render(), 'Hi Mom') + + def test_render__output_encoding(self): + template = Template(u'Poincaré') + actual = template.render('utf-8') + self.assertTrue(isinstance(actual, str)) + self.assertEquals(actual, 'Poincaré') + -- cgit v1.2.1 From 90a0ffc516c7bb9639f0bda0f3cb09fed4e63ea5 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 12:48:18 -0800 Subject: Added a unit test re: non-ascii template characters and added to docstrings. --- pystache/template.py | 11 +++++++++++ tests/test_template.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/pystache/template.py b/pystache/template.py index 471b96e..306aaab 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -68,6 +68,9 @@ class Template(object): Arguments: + template: a template string as a unicode string. Behavior is + undefined if the string has type str. + context: a dictionary, Context, or View instance. load_template: the function for loading partials. The function should @@ -238,6 +241,14 @@ class Template(object): """ Return the template rendered using the current context. + The return value is a unicode string, unless the encoding argument + is not None, in which case the return value has type str (encoded + using that encoding). + + Arguments: + + encoding: the name of the encoding as a string, for example "utf-8". + """ template = self._render_sections(self.template) result = self._render_tags(template) diff --git a/tests/test_template.py b/tests/test_template.py index 4cdf4ad..e01d531 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -44,6 +44,12 @@ class TemplateTestCase(unittest.TestCase): self.assertTrue(isinstance(actual, str)) self.assertEquals(actual, 'foo') + def test_render__non_ascii_character(self): + template = Template(u'Poincaré') + actual = template.render() + self.assertTrue(isinstance(actual, unicode)) + self.assertEquals(actual, u'Poincaré') + def test_render__context(self): template = Template('Hi {{person}}', {'person': 'Mom'}) self.assertEquals(template.render(), 'Hi Mom') -- cgit v1.2.1 From 5302934d567f2c537ba243969a437e9ded0339bf Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 12:56:47 -0800 Subject: Moved the encoding argument from Template.render() to Template.__init__(). --- pystache/template.py | 24 +++++++++++++----------- pystache/view.py | 4 ++-- tests/test_template.py | 3 ++- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 306aaab..97f43d5 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,7 +62,7 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, context=None, load_template=None, **kwargs): + def __init__(self, template=None, context=None, load_template=None, output_encoding=None, **kwargs): """ Construct a Template instance. @@ -77,6 +77,11 @@ class Template(object): accept a single template_name parameter and return a template as a string. Defaults to the default Loader's load_template() method. + output_encoding: the encoding to use when rendering to a string. + The argument should be the name of an encoding as a string, for + example "utf-8". See the render() method's documentation for more + information. + """ if context is None: context = {} @@ -96,6 +101,7 @@ class Template(object): self.context = context self.load_template = load_template + self.output_encoding = output_encoding self.template = template self._compile_regexps() @@ -237,23 +243,19 @@ class Template(object): """ return literal(self.context.get(tag_name, '')) - def render(self, encoding=None): + def render(self): """ Return the template rendered using the current context. - The return value is a unicode string, unless the encoding argument - is not None, in which case the return value has type str (encoded - using that encoding). - - Arguments: - - encoding: the name of the encoding as a string, for example "utf-8". + The return value is a unicode string, unless the output_encoding + attribute is not None, in which case the return value has type str + and is encoded using that encoding. """ template = self._render_sections(self.template) result = self._render_tags(template) - if encoding is not None: - result = result.encode(encoding) + if self.output_encoding is not None: + result = result.encode(self.output_encoding) return result diff --git a/pystache/view.py b/pystache/view.py index 807bb55..0fd7586 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -93,8 +93,8 @@ class View(object): Return the view rendered using the current context. """ - template = Template(self.get_template(), self.context, self.load_template) - return template.render(encoding=encoding) + template = Template(self.get_template(), self.context, self.load_template, output_encoding=encoding) + return template.render() def get(self, key, default=None): return self.context.get(key, default) diff --git a/tests/test_template.py b/tests/test_template.py index e01d531..56db110 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -56,7 +56,8 @@ class TemplateTestCase(unittest.TestCase): def test_render__output_encoding(self): template = Template(u'Poincaré') - actual = template.render('utf-8') + template.output_encoding = 'utf-8' + actual = template.render() self.assertTrue(isinstance(actual, str)) self.assertEquals(actual, 'Poincaré') -- cgit v1.2.1 From 1cb0d1f5564adb67f36a17ffffea4c9e2e6cb15e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 13:28:53 -0800 Subject: Completed issue #52: "Remove the context parameter from Template.__init__()" --- HISTORY.rst | 2 ++ pystache/commands.py | 3 ++- pystache/init.py | 3 ++- pystache/template.py | 36 ++++++++++++++++++------------ pystache/view.py | 4 ++-- tests/test_simple.py | 19 +++++++++------- tests/test_template.py | 59 ++++++++++++++++++++++++++++++++++---------------- 7 files changed, 81 insertions(+), 45 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f1bec2a..b90c448 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,8 @@ History Next Release (version TBD) -------------------------- +* API change: pass the context to render to Template.render() instead of + Template.__init__(). [cjerdonek] * Bugfix: Passing **kwargs to Template() modified the context. [cjerdonek] * Bugfix: Passing **kwargs to Template() with no context raised an exception. [cjerdonek] diff --git a/pystache/commands.py b/pystache/commands.py index 435f286..ac5be88 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -64,7 +64,8 @@ def main(sys_argv): except IOError: context = json.loads(context) - print(Template(template, context).render()) + template = Template(template) + print(template.render(context)) if __name__=='__main__': diff --git a/pystache/init.py b/pystache/init.py index c821aed..4366f69 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -18,4 +18,5 @@ def render(template, context=None, **kwargs): Return the given template string rendered using the given context. """ - return Template(template, context, **kwargs).render() + template = Template(template) + return template.render(context, **kwargs) diff --git a/pystache/template.py b/pystache/template.py index 97f43d5..37568fb 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,7 +62,7 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, context=None, load_template=None, output_encoding=None, **kwargs): + def __init__(self, template=None, load_template=None, output_encoding=None): """ Construct a Template instance. @@ -83,13 +83,23 @@ class Template(object): information. """ - if context is None: - context = {} - if load_template is None: loader = Loader() load_template = loader.load_template - load_template = getattr(context, 'load_template', load_template) + + self.load_template = load_template + self.output_encoding = output_encoding + self.template = template + + self._compile_regexps() + + def _initialize_context(self, context, **kwargs): + """ + Initialize the context attribute. + + """ + if context is None: + context = {} if isinstance(context, Context): context = context.copy() @@ -100,11 +110,7 @@ class Template(object): context.push(kwargs) self.context = context - self.load_template = load_template - self.output_encoding = output_encoding - self.template = template - self._compile_regexps() def _compile_regexps(self): """ @@ -181,8 +187,8 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - template = Template(template, self.context, self.load_template) - out = template.render() + template = Template(template, load_template=self.load_template) + out = template.render(self.context) self.context.pop() @@ -220,8 +226,8 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.load_template(template_name) - template = Template(markup, self.context, self.load_template) - return template.render() + template = Template(markup, load_template=self.load_template) + return template.render(self.context) @modifiers.set('=') def _change_delimiter(self, tag_name): @@ -243,7 +249,7 @@ class Template(object): """ return literal(self.context.get(tag_name, '')) - def render(self): + def render(self, context=None, **kwargs): """ Return the template rendered using the current context. @@ -252,6 +258,8 @@ class Template(object): and is encoded using that encoding. """ + self._initialize_context(context, **kwargs) + template = self._render_sections(self.template) result = self._render_tags(template) diff --git a/pystache/view.py b/pystache/view.py index 0fd7586..1157069 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -93,8 +93,8 @@ class View(object): Return the view rendered using the current context. """ - template = Template(self.get_template(), self.context, self.load_template, output_encoding=encoding) - return template.render() + template = Template(self.get_template(), self.load_template, output_encoding=encoding) + return template.render(self.context) def get(self, key, default=None): return self.context.get(key, default) diff --git a/tests/test_simple.py b/tests/test_simple.py index c01001f..365c82d 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,5 +1,6 @@ import unittest import pystache +from pystache import Template from examples.nested_context import NestedContext from examples.complex_view import ComplexView from examples.lambdas import Lambdas @@ -8,16 +9,15 @@ from examples.simple import Simple class TestSimple(unittest.TestCase): - def test_simple_render(self): - self.assertEqual('herp', pystache.Template('{{derp}}', {'derp': 'herp'}).render()) - def test_nested_context(self): view = NestedContext() - self.assertEquals(pystache.Template('{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}', view).render(), "one and foo and two") + view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}' + self.assertEquals(view.render(), "one and foo and two") def test_looping_and_negation_context(self): view = ComplexView() - self.assertEquals(pystache.Template('{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}', view).render(), "Colors: red Colors: green Colors: blue ") + view.template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' + self.assertEquals(view.render(), "Colors: red Colors: green Colors: blue ") def test_empty_context(self): view = ComplexView() @@ -25,13 +25,16 @@ class TestSimple(unittest.TestCase): def test_callables(self): view = Lambdas() - self.assertEquals(pystache.Template('{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}', view).render(), 'bar != bar. oh, it does!') + view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}' + self.assertEquals(view.render(), 'bar != bar. oh, it does!') def test_rendering_partial(self): view = TemplatePartial() - self.assertEquals(pystache.Template('{{>inner_partial}}', view).render(), 'Again, Welcome!') + view.template = '{{>inner_partial}}' + self.assertEquals(view.render(), 'Again, Welcome!') - self.assertEquals(pystache.Template('{{#looping}}{{>inner_partial}} {{/looping}}', view).render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''') + view.template = '{{#looping}}{{>inner_partial}} {{/looping}}' + self.assertEquals(view.render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''') def test_non_existent_value_renders_blank(self): view = Simple() diff --git a/tests/test_template.py b/tests/test_template.py index 56db110..344dff0 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -15,23 +15,6 @@ class TemplateTestCase(unittest.TestCase): """Test the Template class.""" - def test_init__kwargs_with_no_context(self): - """ - Test passing **kwargs with no context. - - """ - # This test checks that the following line raises no exception. - template = Template(foo="bar") - - def test_init__kwargs_does_not_modify_context(self): - """ - Test that passing **kwargs does not modify the passed context. - - """ - context = {} - template = Template(context=context, foo="bar") - self.assertEquals(context, {}) - def test_render__unicode(self): template = Template(u'foo') actual = template.render() @@ -51,8 +34,46 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(actual, u'Poincaré') def test_render__context(self): - template = Template('Hi {{person}}', {'person': 'Mom'}) - self.assertEquals(template.render(), 'Hi Mom') + """ + Test render(): passing a context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render({'person': 'Mom'}), 'Hi Mom') + + def test_render__context_and_kwargs(self): + """ + Test render(): passing a context and **kwargs. + + """ + template = Template('Hi {{person1}} and {{person2}}') + self.assertEquals(template.render({'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') + + def test_render__kwargs_and_no_context(self): + """ + Test render(): passing **kwargs and no context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render(person='Mom'), 'Hi Mom') + + def test_render__context_and_kwargs__precedence(self): + """ + Test render(): **kwargs takes precedence over context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render({'person': 'Mom'}, person='Dad'), 'Hi Dad') + + def test_render__kwargs_does_not_modify_context(self): + """ + Test render(): passing **kwargs does not modify the passed context. + + """ + context = {} + template = Template('Hi {{person}}') + template.render(context=context, foo="bar") + self.assertEquals(context, {}) def test_render__output_encoding(self): template = Template(u'Poincaré') -- cgit v1.2.1 From c492ee56dbe2e0ba79ab694df2825271c4ce98af Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 13:38:46 -0800 Subject: Some corrections to Template docstrings. --- pystache/template.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 37568fb..8f40bfa 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -71,8 +71,6 @@ class Template(object): template: a template string as a unicode string. Behavior is undefined if the string has type str. - context: a dictionary, Context, or View instance. - load_template: the function for loading partials. The function should accept a single template_name parameter and return a template as a string. Defaults to the default Loader's load_template() method. @@ -257,6 +255,13 @@ class Template(object): attribute is not None, in which case the return value has type str and is encoded using that encoding. + Arguments: + + context: a dictionary, Context, or object (e.g. a View instance). + + **kwargs: additional key values to add to the context when rendering. + These values take precedence over the context on any key conflicts. + """ self._initialize_context(context, **kwargs) -- cgit v1.2.1 From b8ed0b672a5843f4e8676d0a1afcf6fb79cbccd0 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 15:08:06 -0800 Subject: Tightened up the code in Template._render_tags(). * Used re.split() instead of re.search() to avoid string slicing. * Used one "".join() instead of repeated string concatenation. * Avoided duplicate logic on loop exit (i.e. output += template). --- pystache/template.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ad835ad..526e53f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -169,21 +169,27 @@ class Template(object): return template def _render_tags(self, template): - output = '' + output = [] while True: - match = self.tag_re.search(template) - if match is None: + parts = self.tag_re.split(template, maxsplit=1) + output.append(parts[0]) + + if len(parts) < 2: + # Then there was no match. break - tag, tag_type, tag_name = match.group(0, 1, 2) + start, tag_type, tag_name, template = parts + tag_name = tag_name.strip() func = self.modifiers[tag_type] - replacement = func(self, tag_name) - output = output + template[0:match.start()] + replacement - template = template[match.end():] + tag_value = func(self, tag_name) + + # Appending the tag value to the output prevents treating the + # value as a template string (bug: issue #44). + output.append(tag_value) - output = output + template + output = "".join(output) return output def _render_dictionary(self, template, context): -- cgit v1.2.1 From fc4882cf9bde259c7564789c0a2a25db87e31561 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 15:13:26 -0800 Subject: Updated history notes for issue #44. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index b90c448..533211b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,7 @@ History Next Release (version TBD) -------------------------- +* Bugfix: context values no longer processed as template strings. [jakearchibald] * API change: pass the context to render to Template.render() instead of Template.__init__(). [cjerdonek] * Bugfix: Passing **kwargs to Template() modified the context. [cjerdonek] -- cgit v1.2.1 From 04631c0521dbc48397bb96000620bb09b66828f4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 16:44:32 -0800 Subject: Renamed section_name to section_key. --- pystache/template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ff41308..fac0f68 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -138,9 +138,9 @@ class Template(object): if match is None: break - section, section_name, inner = match.group(0, 1, 2) - section_name = section_name.strip() - it = self.context.get(section_name, None) + section, section_key, inner = match.group(0, 1, 2) + section_key = section_key.strip() + it = self.context.get(section_key, None) replacer = '' # Callable -- cgit v1.2.1 From fd3d97fdeb1155d65f264c0195fd1debfb58f283 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 16:46:11 -0800 Subject: Renamed "it" to section_value. --- pystache/template.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index fac0f68..4247b8f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -140,31 +140,31 @@ class Template(object): section, section_key, inner = match.group(0, 1, 2) section_key = section_key.strip() - it = self.context.get(section_key, None) + section_value = self.context.get(section_key, None) replacer = '' # Callable - if it and check_callable(it): - replacer = it(inner) + if section_value and check_callable(section_value): + replacer = section_value(inner) # Dictionary - elif it and hasattr(it, 'keys') and hasattr(it, '__getitem__'): + elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): if section[2] != '^': - replacer = self._render_dictionary(inner, it) + replacer = self._render_dictionary(inner, section_value) # Lists - elif it and hasattr(it, '__iter__'): + elif section_value and hasattr(section_value, '__iter__'): if section[2] != '^': - replacer = self._render_list(inner, it) + replacer = self._render_list(inner, section_value) # Other objects - elif it and isinstance(it, object): + elif section_value and isinstance(section_value, object): if section[2] != '^': - replacer = self._render_dictionary(inner, it) + replacer = self._render_dictionary(inner, section_value) # Falsey and Negated or Truthy and Not Negated - elif (not it and section[2] == '^') or (it and section[2] != '^'): - replacer = self._render_dictionary(inner, it) + elif (not section_value and section[2] == '^') or (section_value and section[2] != '^'): + replacer = self._render_dictionary(inner, section_value) # Render template prior to section too output = output + self._render_tags(template[0:match.start()]) + replacer -- cgit v1.2.1 From c2fcba500f7a9667c39667e4fb88b5a48de5303f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 16:59:47 -0800 Subject: Renamed inner to section_contents. --- pystache/template.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 4247b8f..d57cb7c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -138,41 +138,41 @@ class Template(object): if match is None: break - section, section_key, inner = match.group(0, 1, 2) + section, section_key, section_contents = match.group(0, 1, 2) section_key = section_key.strip() section_value = self.context.get(section_key, None) replacer = '' # Callable if section_value and check_callable(section_value): - replacer = section_value(inner) + replacer = section_value(section_contents) # Dictionary elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): if section[2] != '^': - replacer = self._render_dictionary(inner, section_value) + replacer = self._render_dictionary(section_contents, section_value) # Lists elif section_value and hasattr(section_value, '__iter__'): if section[2] != '^': - replacer = self._render_list(inner, section_value) + replacer = self._render_list(section_contents, section_value) # Other objects elif section_value and isinstance(section_value, object): if section[2] != '^': - replacer = self._render_dictionary(inner, section_value) + replacer = self._render_dictionary(section_contents, section_value) # Falsey and Negated or Truthy and Not Negated elif (not section_value and section[2] == '^') or (section_value and section[2] != '^'): - replacer = self._render_dictionary(inner, section_value) + replacer = self._render_dictionary(section_contents, section_value) # Render template prior to section too - output = output + self._render_tags(template[0:match.start()]) + replacer + output += self._render_tags(template[0:match.start()]) + replacer template = template[match.end():] # Render remainder - output = output + self._render_tags(template) + output += self._render_tags(template) return output -- cgit v1.2.1 From 369faa88272f577022ff075dbc30c83630b490a0 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 17:55:38 -0800 Subject: Tightened up code in Template._render(). * Used re.split() instead of re.search() to avoid string slicing. * Used "".join() once instead of concatenating strings. * Removed the need for repeating logic on loop exit (i.e. an additional application of self._render_tags(template) and output += ...). --- pystache/template.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d57cb7c..d97f80e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -124,23 +124,30 @@ class Template(object): # The section contents include white space to comply with the spec's # requirement that sections not alter surrounding whitespace. - section = r"%(otag)s[\#|^]([^\}]*)%(ctag)s(.+?)%(otag)s/\1%(ctag)s" - self.section_re = re.compile(section % tags, re.M|re.S) + section = r"%(otag)s([#|^])([^\}]*)%(ctag)s(.+?)%(otag)s/\2%(ctag)s" % tags + self.section_re = re.compile(section, re.M|re.S) - tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" - self.tag_re = re.compile(tag % tags) + tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" % tags + self.tag_re = re.compile(tag) def _render(self, template): - output = '' + output = [] while True: - match = self.section_re.search(template) - if match is None: + parts = self.section_re.split(template, maxsplit=1) + + start = self._render_tags(parts[0]) + output.append(start) + + if len(parts) < 2: + # Then there was no match. break - section, section_key, section_contents = match.group(0, 1, 2) + section_type, section_key, section_contents, template = parts[1:] + section_key = section_key.strip() section_value = self.context.get(section_key, None) + replacer = '' # Callable @@ -149,31 +156,27 @@ class Template(object): # Dictionary elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): - if section[2] != '^': + if section_type != '^': replacer = self._render_dictionary(section_contents, section_value) # Lists elif section_value and hasattr(section_value, '__iter__'): - if section[2] != '^': + if section_type != '^': replacer = self._render_list(section_contents, section_value) # Other objects elif section_value and isinstance(section_value, object): - if section[2] != '^': + if section_type != '^': replacer = self._render_dictionary(section_contents, section_value) # Falsey and Negated or Truthy and Not Negated - elif (not section_value and section[2] == '^') or (section_value and section[2] != '^'): + elif (not section_value and section_type == '^') or (section_value and section_type != '^'): replacer = self._render_dictionary(section_contents, section_value) # Render template prior to section too - output += self._render_tags(template[0:match.start()]) + replacer - - template = template[match.end():] - - # Render remainder - output += self._render_tags(template) + output.append(replacer) + output = "".join(output) return output def _render_tags(self, template): -- cgit v1.2.1 From c17182965eefaf8b4694dc9b8de0827c527810e1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 18:00:18 -0800 Subject: Renamed "replacer" to "rendered". --- pystache/template.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d97f80e..6b87d07 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -148,33 +148,33 @@ class Template(object): section_key = section_key.strip() section_value = self.context.get(section_key, None) - replacer = '' + rendered = '' # Callable if section_value and check_callable(section_value): - replacer = section_value(section_contents) + rendered = section_value(section_contents) # Dictionary elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): if section_type != '^': - replacer = self._render_dictionary(section_contents, section_value) + rendered = self._render_dictionary(section_contents, section_value) # Lists elif section_value and hasattr(section_value, '__iter__'): if section_type != '^': - replacer = self._render_list(section_contents, section_value) + rendered = self._render_list(section_contents, section_value) # Other objects elif section_value and isinstance(section_value, object): if section_type != '^': - replacer = self._render_dictionary(section_contents, section_value) + rendered = self._render_dictionary(section_contents, section_value) # Falsey and Negated or Truthy and Not Negated elif (not section_value and section_type == '^') or (section_value and section_type != '^'): - replacer = self._render_dictionary(section_contents, section_value) + rendered = self._render_dictionary(section_contents, section_value) # Render template prior to section too - output.append(replacer) + output.append(rendered) output = "".join(output) return output -- cgit v1.2.1 From 120d42254165dd9088ea155d20e5318b68f23c6f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 19:01:11 -0800 Subject: Added to the Template class support for disabling HTML escaping. --- pystache/template.py | 18 ++++++++++++++---- tests/test_template.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 6b87d07..7a2bc8b 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -62,7 +62,8 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, load_template=None, output_encoding=None): + def __init__(self, template=None, load_template=None, output_encoding=None, + disable_escape=False): """ Construct a Template instance. @@ -85,12 +86,19 @@ class Template(object): loader = Loader() load_template = loader.load_template + self.disable_escape = disable_escape self.load_template = load_template self.output_encoding = output_encoding self.template = template self._compile_regexps() + def escape(self, text): + return escape(text) + + def literal(self, text): + return literal(text) + def _initialize_context(self, context, **kwargs): """ Initialize the context attribute. @@ -206,7 +214,7 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - template = Template(template, load_template=self.load_template) + template = Template(template, load_template=self.load_template, disable_escape=self.disable_escape) out = template.render(self.context) self.context.pop() @@ -236,7 +244,7 @@ class Template(object): else: return '' - return escape(raw) + return self._render_value(raw) @modifiers.set('!') def _render_comment(self, tag_name): @@ -245,7 +253,7 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.load_template(template_name) - template = Template(markup, load_template=self.load_template) + template = Template(markup, load_template=self.load_template, disable_escape=self.disable_escape) return template.render(self.context) @modifiers.set('=') @@ -286,6 +294,8 @@ class Template(object): """ self._initialize_context(context, **kwargs) + self._render_value = self.literal if self.disable_escape else self.escape + result = self._render(self.template) if self.output_encoding is not None: diff --git a/tests/test_template.py b/tests/test_template.py index 71516df..866e13f 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -15,6 +15,14 @@ class TemplateTestCase(unittest.TestCase): """Test the Template class.""" + def test_init__disable_escape(self): + # Test default value. + template = Template() + self.assertEquals(template.disable_escape, False) + + template = Template(disable_escape=True) + self.assertEquals(template.disable_escape, True) + def test_render__unicode(self): template = Template(u'foo') actual = template.render() @@ -117,3 +125,37 @@ class TemplateTestCase(unittest.TestCase): context = {'test': (lambda text: '{{hi}} %s' % text)} actual = template.render(context) self.assertEquals(actual, '{{hi}} Mom') + + def test_render__html_escape(self): + context = {'test': '1 < 2'} + template = Template('{{test}}') + + self.assertEquals(template.render(context), '1 < 2') + + def test_render__html_escape_disabled(self): + context = {'test': '1 < 2'} + template = Template('{{test}}') + + self.assertEquals(template.render(context), '1 < 2') + + template.disable_escape = True + self.assertEquals(template.render(context), '1 < 2') + + def test_render__html_escape_disabled_with_partial(self): + context = {'test': '1 < 2'} + load_template = lambda name: '{{test}}' + template = Template('{{>partial}}', load_template=load_template) + + self.assertEquals(template.render(context), '1 < 2') + + template.disable_escape = True + self.assertEquals(template.render(context), '1 < 2') + + def test_render__html_escape_disabled_with_non_false_value(self): + context = {'section': {'test': '1 < 2'}} + template = Template('{{#section}}{{test}}{{/section}}') + + self.assertEquals(template.render(context), '1 < 2') + + template.disable_escape = True + self.assertEquals(template.render(context), '1 < 2') -- cgit v1.2.1 From 172674135e8a24f7786733d2adf9da12326d6a2b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 19:07:05 -0800 Subject: Added to the View class support for disable_escape. --- pystache/view.py | 5 +++-- tests/test_examples.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 1157069..897444c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -88,12 +88,13 @@ class View(object): return re.sub('[A-Z]', repl, template_name)[1:] - def render(self, encoding=None): + def render(self, encoding=None, disable_escape=False): """ Return the view rendered using the current context. """ - template = Template(self.get_template(), self.load_template, output_encoding=encoding) + template = Template(self.get_template(), self.load_template, output_encoding=encoding, + disable_escape=disable_escape) return template.render(self.context) def get(self, key, default=None): diff --git a/tests/test_examples.py b/tests/test_examples.py index 63267cc..20abe88 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -34,6 +34,9 @@ class TestView(unittest.TestCase): def test_escaped(self): self.assertEquals(Escaped().render(), "

Bear > Shark

") + def test_escaped_disabling(self): + self.assertEquals(Escaped().render(disable_escape=True), "

Bear > Shark

") + def test_unescaped(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") -- cgit v1.2.1 From 5bdba9c5d0d2b10c3f4009b2fe844bac4424f286 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 19:09:18 -0800 Subject: Updated history notes for issue #32. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 533211b..74061a6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,7 @@ History Next Release (version TBD) -------------------------- +* Feature: Template and View support disabling HTML escape. [cjerdonek] * Bugfix: context values no longer processed as template strings. [jakearchibald] * API change: pass the context to render to Template.render() instead of Template.__init__(). [cjerdonek] -- cgit v1.2.1 From 648d0463cb655267673efc32ab124039caeff7fa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 19:15:40 -0800 Subject: Organized history notes by category. --- HISTORY.rst | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 74061a6..51d4793 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,19 +3,25 @@ History Next Release (version TBD) -------------------------- -* Feature: Template and View support disabling HTML escape. [cjerdonek] -* Bugfix: context values no longer processed as template strings. [jakearchibald] -* API change: pass the context to render to Template.render() instead of - Template.__init__(). [cjerdonek] -* Bugfix: Passing **kwargs to Template() modified the context. [cjerdonek] -* Bugfix: Passing **kwargs to Template() with no context raised an - exception. [cjerdonek] -* Bugfix: Whitespace surrounding sections is no longer altered, in - accordance with the mustache spec. [heliodor] + +Features: +* Support for disabling HTML escape in Template and View classes. [cjerdonek] * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] -* Bugfix: Fixed an issue that affected the rendering of zeroes when using - certain implementations of Python (i.e. PyPy). [alex] + +API changes: +* Template.render() now accepts the context instead of Template.__init__(). [cjerdonek] + +Bug fixes: +* Context values no longer processed as template strings. [jakearchibald] +* Passing **kwargs to Template() modified the context. [cjerdonek] +* Passing **kwargs to Template() with no context raised an exception. [cjerdonek] +* Whitespace surrounding sections is no longer altered, in accordance with + the mustache spec. [heliodor] +* Fixed an issue that affected the rendering of zeroes when using certain + implementations of Python (i.e. PyPy). [alex] + +Misc: * Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) -- cgit v1.2.1 From 9b087b282ce0d3061e81d12eb16b6c73ac09afea Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 19:29:54 -0800 Subject: Fixed history file rendering issues (due to reStructuredText syntax). --- HISTORY.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 51d4793..5f7dd10 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,23 +5,28 @@ Next Release (version TBD) -------------------------- Features: + * Support for disabling HTML escape in Template and View classes. [cjerdonek] * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] API changes: -* Template.render() now accepts the context instead of Template.__init__(). [cjerdonek] + +* ``Template.render()`` now accepts the context to render instead of + ``Template()``. [cjerdonek] Bug fixes: + * Context values no longer processed as template strings. [jakearchibald] -* Passing **kwargs to Template() modified the context. [cjerdonek] -* Passing **kwargs to Template() with no context raised an exception. [cjerdonek] +* Passing ``**kwargs`` to ``Template()`` modified the context. [cjerdonek] +* Passing ``**kwargs`` to ``Template()`` with no context raised an exception. [cjerdonek] * Whitespace surrounding sections is no longer altered, in accordance with the mustache spec. [heliodor] * Fixed an issue that affected the rendering of zeroes when using certain implementations of Python (i.e. PyPy). [alex] Misc: + * Added some docstrings. [kennethreitz] 0.4.0 (2011-01-12) -- cgit v1.2.1 From cc18da306f6a583b68c15bb896a875a403de876f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 20:01:53 -0800 Subject: Fixed issue #40. --- examples/extensionless | 1 + pystache/loader.py | 13 ++++++++++++- tests/test_loader.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 examples/extensionless diff --git a/examples/extensionless b/examples/extensionless new file mode 100644 index 0000000..452c9fe --- /dev/null +++ b/examples/extensionless @@ -0,0 +1 @@ +No file extension: {{foo}} \ No newline at end of file diff --git a/pystache/loader.py b/pystache/loader.py index 9e59774..83690d2 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -22,6 +22,9 @@ class Loader(object): search_dirs: the directories in which to search for templates. Defaults to the current working directory. + extension: the template file extension. Defaults to "mustache". + Pass False for no extension. + """ if extension is None: extension = DEFAULT_EXTENSION @@ -35,6 +38,13 @@ class Loader(object): self.template_encoding = encoding self.template_extension = extension + def make_file_name(self, template_name): + file_name = template_name + if self.template_extension is not False: + file_name += os.path.extsep + self.template_extension + + return file_name + def load_template(self, template_name): """ Find and load the given template, and return it as a string. @@ -43,7 +53,8 @@ class Loader(object): """ search_dirs = self.search_dirs - file_name = template_name + '.' + self.template_extension + + file_name = self.make_file_name(template_name) for dir_path in search_dirs: file_path = os.path.join(dir_path, file_name) diff --git a/tests/test_loader.py b/tests/test_loader.py index 7071d2b..70aacd9 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -6,6 +6,8 @@ from pystache.loader import Loader class LoaderTestCase(unittest.TestCase): + search_dirs = 'examples' + def test_init(self): """ Test the __init__() constructor. @@ -14,13 +16,34 @@ class LoaderTestCase(unittest.TestCase): loader = Loader() self.assertEquals(loader.search_dirs, [os.curdir]) self.assertTrue(loader.template_encoding is None) - self.assertEquals(loader.template_extension, 'mustache') - loader = Loader(search_dirs=['foo'], encoding='utf-8', extension='txt') + loader = Loader(search_dirs=['foo'], encoding='utf-8') self.assertEquals(loader.search_dirs, ['foo']) self.assertEquals(loader.template_encoding, 'utf-8') + + def test_init__extension(self): + # Test the default value. + loader = Loader() + self.assertEquals(loader.template_extension, 'mustache') + + loader = Loader(extension='txt') self.assertEquals(loader.template_extension, 'txt') + loader = Loader(extension=False) + self.assertTrue(loader.template_extension is False) + + def test_make_file_name(self): + loader = Loader() + + loader.template_extension = 'bar' + self.assertEquals(loader.make_file_name('foo'), 'foo.bar') + + loader.template_extension = False + self.assertEquals(loader.make_file_name('foo'), 'foo') + + loader.template_extension = '' + self.assertEquals(loader.make_file_name('foo'), 'foo.') + def test_template_is_loaded(self): loader = Loader(search_dirs='examples') template = loader.load_template('simple') @@ -37,3 +60,10 @@ class LoaderTestCase(unittest.TestCase): loader = Loader() self.assertRaises(IOError, loader.load_template, 'doesnt_exist') + + def test_load_template__extensionless_file(self): + loader = Loader(search_dirs=self.search_dirs) + self.assertRaises(IOError, loader.load_template, 'extensionless') + + loader.template_extension = False + self.assertEquals(loader.load_template('extensionless'), "No file extension: {{foo}}") -- cgit v1.2.1 From 35d291dd336fd2219c4a39d81cc3a38456c00d67 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 20:07:14 -0800 Subject: Added a View unit test for extensionless template files. --- tests/test_view.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_view.py b/tests/test_view.py index b68da2b..0402f6d 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -54,6 +54,12 @@ class ViewTestCase(unittest.TestCase): template = Simple().load_template("escaped") self.assertEquals(template, "

{{title}}

") + def test_load_template__extensionless_file(self): + view = Simple() + view.template_extension = False + template = view.load_template('extensionless') + self.assertEquals(template, "No file extension: {{foo}}") + def test_custom_load_template(self): """ Test passing a custom load_template to View.__init__(). -- cgit v1.2.1 From 39875728e812dd3c5841181096281f225d40dc2f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 20:09:11 -0800 Subject: Updated history notes for issue #40. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 5f7dd10..f8e5f96 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,6 +24,7 @@ Bug fixes: the mustache spec. [heliodor] * Fixed an issue that affected the rendering of zeroes when using certain implementations of Python (i.e. PyPy). [alex] +* Extensionless template files could not be loaded. [cjerdonek] Misc: -- cgit v1.2.1 From e18826eded78e749ceed99e52e1accb3db1fb4c2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Dec 2011 21:11:09 -0800 Subject: Added a passing unit test for issue #8. --- tests/test_template.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_template.py b/tests/test_template.py index 866e13f..a463ad4 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -159,3 +159,20 @@ class TemplateTestCase(unittest.TestCase): template.disable_escape = True self.assertEquals(template.render(context), '1 < 2') + + def test_render__list_referencing_outer_context(self): + """ + Check that list items can access the parent context. + + For sections whose value is a list, check that items in the list + have access to the values inherited from the parent context + when rendering. + + """ + context = { + "list": [{"name": "Al"}, {"name": "Bo"}], + "greeting": "Hi", + } + template = Template("{{#list}}{{name}}: {{greeting}}; {{/list}}") + + self.assertEquals(template.render(context), "Al: Hi; Bo: Hi; ") -- cgit v1.2.1 From 6e6f399feae5179d98ef370e920fccdaf6fd6f8c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Dec 2011 07:34:50 -0800 Subject: Fixed oversight: literal->self.literal. --- pystache/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index 7a2bc8b..fc1eec2 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -274,7 +274,7 @@ class Template(object): Render a tag without escaping it. """ - return literal(self.context.get(tag_name, '')) + return self.literal(self.context.get(tag_name, '')) def render(self, context=None, **kwargs): """ -- cgit v1.2.1 From f54aa6ff512a19b2398891aa16ffe26155cf6c26 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Dec 2011 13:27:09 -0800 Subject: Fixed issue #54: markupsafe can now be turned on and off. The above change was initially made for testing purpose. The unit tests now run the test cases in test_pystache.py twice if markupsafe is available: once with markupsafe enabled and once without. All unit tests now pass using markupsafe (there was previously one failing test in test_pystache.py: test_non_strings). --- pystache/template.py | 21 +++++++++++---------- tests/test_pystache.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index fc1eec2..1d8599d 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -13,13 +13,11 @@ from .context import Context from .loader import Loader +markupsafe = None try: import markupsafe - escape = markupsafe.escape - literal = markupsafe.Markup except ImportError: - escape = lambda x: cgi.escape(unicode(x)) - literal = unicode + pass try: @@ -86,19 +84,22 @@ class Template(object): loader = Loader() load_template = loader.load_template + if markupsafe: + escape = markupsafe.escape + literal = markupsafe.Markup + else: + escape = lambda x: cgi.escape(unicode(x)) + literal = unicode + self.disable_escape = disable_escape + self.escape = escape + self.literal = literal self.load_template = load_template self.output_encoding = output_encoding self.template = template self._compile_regexps() - def escape(self, text): - return escape(text) - - def literal(self, text): - return literal(text) - def _initialize_context(self, context, **kwargs): """ Initialize the context attribute. diff --git a/tests/test_pystache.py b/tests/test_pystache.py index 5bb88ed..a01d7cd 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -2,8 +2,19 @@ import unittest import pystache +from pystache import template + + +class PystacheTests(object): + + """ + Contains tests to run with markupsafe both enabled and disabled. + + To run the tests in this class, this class should be subclassed by + a class that implements unittest.TestCase. + + """ -class TestPystache(unittest.TestCase): def test_basic(self): ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) self.assertEquals(ret, "Hi world!") @@ -47,6 +58,8 @@ class TestPystache(unittest.TestCase): ret = pystache.render(template, { 'set': True }) self.assertEquals(ret, "Ready set go!") + non_strings_expected = """(123 & ['something'])(chris & 0.9)""" + def test_non_strings(self): template = "{{#stats}}({{key}} & {{value}}){{/stats}}" stats = [] @@ -54,7 +67,7 @@ class TestPystache(unittest.TestCase): stats.append({'key': u"chris", 'value': 0.900}) ret = pystache.render(template, { 'stats': stats }) - self.assertEquals(ret, """(123 & ['something'])(chris & 0.9)""") + self.assertEquals(ret, self.non_strings_expected) def test_unicode(self): template = 'Name: {{name}}; Age: {{age}}' @@ -80,3 +93,30 @@ class TestPystache(unittest.TestCase): template = "first{{#spacing}} second {{/spacing}}third" ret = pystache.render(template, {"spacing": True}) self.assertEquals(ret, "first second third") + + +class PystacheWithoutMarkupsafeTests(PystacheTests, unittest.TestCase): + + """Test pystache without markupsafe enabled.""" + + def setUp(self): + self.original_markupsafe = template.markupsafe + template.markupsafe = None + + def tearDown(self): + template.markupsafe = self.original_markupsafe + + +# If markupsafe is available, then run the same tests again but without +# disabling markupsafe. +_BaseClass = unittest.TestCase if template.markupsafe else object +class PystacheWithMarkupsafeTests(PystacheTests, _BaseClass): + + """Test pystache with markupsafe enabled.""" + + # markupsafe.escape() escapes single quotes: "'" becomes "'". + non_strings_expected = """(123 & ['something'])(chris & 0.9)""" + + def test_markupsafe_available(self): + self.assertTrue(template.markupsafe, "markupsafe isn't available. " + "The with-markupsafe tests shouldn't be running.") -- cgit v1.2.1 From cdd4484fe300cf37f88fec3a3bc78cd314a2ba94 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Dec 2011 13:29:57 -0800 Subject: Updated history notes for issue #54. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index f8e5f96..5e7bc96 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,7 @@ Features: * Support for disabling HTML escape in Template and View classes. [cjerdonek] * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] +* Markupsafe can now be disabled after import. [cjerdonek] API changes: -- cgit v1.2.1 From 7e130676d3523db4e936e5f8f726c1493b2cbd31 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Dec 2011 19:07:32 -0800 Subject: Added simpler test case for issue #53. --- tests/test_pystache.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index c04489b..5954185 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -68,12 +68,27 @@ class TestPystache(unittest.TestCase): context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") - + def test_implicit_iterator(self): template = """
    {{#users}}
  • {{.}}
  • {{/users}}
""" context = { 'users': [ 'Chris', 'Tom','PJ' ] } ret = pystache.render(template, context) self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") + def test_later_list_section_with_escapable_character(self): + """ + This is a simple test case intended to cover issue #53. + + The incorrect result was the following, when markupsafe is available + (unnecessary escaping of the second section's contents)-- + + AssertionError: Markup(u'foo <') != 'foo <' + + """ + template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}""" + context = {'s1': True, 's2': [True]} + actual = pystache.render(template, context) + self.assertEquals(actual, """foo <""") + if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From f2108401fc4a32d27123b004829d321af7478c04 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 17:16:37 -0800 Subject: Fixed issue #56: Template() now accepts an "escape" argument. From the issue: The constructor `Template.__init__()` should accept an optional `escape` function argument instead of the boolean `disable_escape`. This is more natural. Examples: * Users wanting to disable escaping can pass `lambda s: s`. * Non-markupsafe users wanting double-quotes escaped can pass `lambda s: cgi.escape(s, True)`. The cgi.escape() function doesn't escape double-quotes by default. * Similarly, markupsafe users can specify an alternative to `markupsafe.escape()`. --- pystache/template.py | 38 ++++++++++++++++++++++++-------------- pystache/view.py | 9 +++++++-- tests/test_examples.py | 11 ++++++----- tests/test_template.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 1d8599d..3354801 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -60,8 +60,7 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, load_template=None, output_encoding=None, - disable_escape=False): + def __init__(self, template=None, load_template=None, output_encoding=None, escape=None): """ Construct a Template instance. @@ -79,19 +78,27 @@ class Template(object): example "utf-8". See the render() method's documentation for more information. + escape: the function used to escape mustache variable values + when rendering a template. The function should accept a unicode + string and return an escaped string of the same type. It need + not handle strings of type `str` because this class will only + pass it unicode strings. The constructor assigns this escape + function to the constructed instance's Template.escape() method. + + The argument defaults to markupsafe.escape when markupsafe is + importable and cgi.escape otherwise. To disable escaping entirely, + one can pass `lambda s: s` as the escape function, for example. + """ if load_template is None: loader = Loader() load_template = loader.load_template - if markupsafe: - escape = markupsafe.escape - literal = markupsafe.Markup - else: - escape = lambda x: cgi.escape(unicode(x)) - literal = unicode + if escape is None: + escape = markupsafe.escape if markupsafe else cgi.escape + + literal = markupsafe.Markup if markupsafe else unicode - self.disable_escape = disable_escape self.escape = escape self.literal = literal self.load_template = load_template @@ -100,6 +107,11 @@ class Template(object): self._compile_regexps() + def _unicode_and_escape(self, s): + if not isinstance(s, unicode): + s = unicode(s) + return self.escape(s) + def _initialize_context(self, context, **kwargs): """ Initialize the context attribute. @@ -215,7 +227,7 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - template = Template(template, load_template=self.load_template, disable_escape=self.disable_escape) + template = Template(template, load_template=self.load_template, escape=self.escape) out = template.render(self.context) self.context.pop() @@ -245,7 +257,7 @@ class Template(object): else: return '' - return self._render_value(raw) + return self._unicode_and_escape(raw) @modifiers.set('!') def _render_comment(self, tag_name): @@ -254,7 +266,7 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.load_template(template_name) - template = Template(markup, load_template=self.load_template, disable_escape=self.disable_escape) + template = Template(markup, load_template=self.load_template, escape=self.escape) return template.render(self.context) @modifiers.set('=') @@ -295,8 +307,6 @@ class Template(object): """ self._initialize_context(context, **kwargs) - self._render_value = self.literal if self.disable_escape else self.escape - result = self._render(self.template) if self.output_encoding is not None: diff --git a/pystache/view.py b/pystache/view.py index 897444c..fa8ac47 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -88,13 +88,18 @@ class View(object): return re.sub('[A-Z]', repl, template_name)[1:] - def render(self, encoding=None, disable_escape=False): + # TODO: the View class should probably have some sort of template renderer + # associated with it to encapsulate all of the render-specific behavior + # and options like encoding, escape, etc. This would probably be better + # than passing all of these options to render(), especially as the list + # of possible options grows. + def render(self, encoding=None, escape=None): """ Return the view rendered using the current context. """ template = Template(self.get_template(), self.load_template, output_encoding=encoding, - disable_escape=disable_escape) + escape=escape) return template.render(self.context) def get(self, key, default=None): diff --git a/tests/test_examples.py b/tests/test_examples.py index 20abe88..4abe673 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -31,16 +31,17 @@ class TestView(unittest.TestCase): self.assertEquals(UnicodeInput().render(), u'

If alive today, Henri Poincaré would be 156 years old.

') - def test_escaped(self): + def test_escaping(self): self.assertEquals(Escaped().render(), "

Bear > Shark

") - def test_escaped_disabling(self): - self.assertEquals(Escaped().render(disable_escape=True), "

Bear > Shark

") + def test_escaping__custom(self): + escape = lambda s: s.upper() + self.assertEquals(Escaped().render(escape=escape), "

BEAR > SHARK

") - def test_unescaped(self): + def test_literal(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") - def test_unescaped_sigil(self): + def test_literal_sigil(self): view = Escaped(template="

{{& thing}}

", context={ 'thing': 'Bear > Giraffe' }) diff --git a/tests/test_template.py b/tests/test_template.py index a463ad4..9324a9d 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -8,6 +8,7 @@ Unit tests of template.py. import codecs import unittest +from pystache import template from pystache.template import Template @@ -15,13 +16,44 @@ class TemplateTestCase(unittest.TestCase): """Test the Template class.""" - def test_init__disable_escape(self): - # Test default value. + def setUp(self): + """ + Disable markupsafe. + + """ + self.original_markupsafe = template.markupsafe + template.markupsafe = None + + def tearDown(self): + self._restore_markupsafe() + + def _was_markupsafe_imported(self): + return bool(self.original_markupsafe) + + def _restore_markupsafe(self): + """ + Restore markupsafe to its original state. + + """ + template.markupsafe = self.original_markupsafe + + def test_init__escape__default_without_markupsafe(self): + template = Template() + self.assertEquals(template.escape(">'"), ">'") + + def test_init__escape__default_with_markupsafe(self): + if not self._was_markupsafe_imported(): + # Then we cannot test this case. + return + self._restore_markupsafe() + template = Template() - self.assertEquals(template.disable_escape, False) + self.assertEquals(template.escape(">'"), ">'") - template = Template(disable_escape=True) - self.assertEquals(template.disable_escape, True) + def test_init__escape(self): + escape = lambda s: "foo" + s + template = Template(escape=escape) + self.assertEquals(template.escape("bar"), "foobar") def test_render__unicode(self): template = Template(u'foo') @@ -138,7 +170,7 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(template.render(context), '1 < 2') - template.disable_escape = True + template.escape = lambda s: s self.assertEquals(template.render(context), '1 < 2') def test_render__html_escape_disabled_with_partial(self): @@ -148,7 +180,7 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(template.render(context), '1 < 2') - template.disable_escape = True + template.escape = lambda s: s self.assertEquals(template.render(context), '1 < 2') def test_render__html_escape_disabled_with_non_false_value(self): @@ -157,7 +189,7 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(template.render(context), '1 < 2') - template.disable_escape = True + template.escape = lambda s: s self.assertEquals(template.render(context), '1 < 2') def test_render__list_referencing_outer_context(self): -- cgit v1.2.1 From 3dd53c1a70545647caa4594317b24eefbb8727fc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 18:36:45 -0800 Subject: Addressed issue #55: Add to Template() support for "encoding" and "errors" Added two keyword arguments to Template.__init__(): "default_encoding" and "decode_errors". These are passed internally to unicode() as the "encoding" and "errors" arguments when converting strings of type str to unicode. --- pystache/template.py | 50 +++++++++++++++++++++++++++-- tests/test_template.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 3354801..9a150f1 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -5,9 +5,10 @@ This module provides a Template class. """ -import re import cgi import collections +import re +import sys from .context import Context from .loader import Loader @@ -60,7 +61,8 @@ class Template(object): modifiers = Modifiers() - def __init__(self, template=None, load_template=None, output_encoding=None, escape=None): + def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, + default_encoding=None, decode_errors='strict'): """ Construct a Template instance. @@ -89,18 +91,35 @@ class Template(object): importable and cgi.escape otherwise. To disable escaping entirely, one can pass `lambda s: s` as the escape function, for example. + default_encoding: the name of the encoding to use when converting + to unicode any strings of type `str` encountered during the + rendering process. The name will be passed as the "encoding" + argument to the built-in function unicode(). Defaults to the + encoding name returned by sys.getdefaultencoding(). + + decode_errors: the string to pass as the "errors" argument to the + built-in function unicode() when converting to unicode any + strings of type `str` encountered during the rendering process. + Defaults to "strict". + """ if load_template is None: loader = Loader() load_template = loader.load_template + if default_encoding is None: + default_encoding = sys.getdefaultencoding() + if escape is None: escape = markupsafe.escape if markupsafe else cgi.escape literal = markupsafe.Markup if markupsafe else unicode + self._literal = literal + + self.decode_errors = decode_errors + self.default_encoding = default_encoding self.escape = escape - self.literal = literal self.load_template = load_template self.output_encoding = output_encoding self.template = template @@ -112,6 +131,31 @@ class Template(object): s = unicode(s) return self.escape(s) + def escape(self, u): + """ + Escape a unicode string, and return it. + + This function is initialized as the escape function that was passed + to the Template class's constructor when this instance was + constructed. See the constructor docstring for more information. + + """ + pass + + + def literal(self, s): + """ + Convert the given string to a unicode string, without escaping it. + + This function internally calls the built-in function unicode() and + passes it the default_encoding and decode_errors attributes for this + Template instance. If markupsafe was importable when loading this + module, this function returns an instance of the class + markupsafe.Markup (which subclasses unicode). + + """ + return self._literal(s, self.default_encoding, self.decode_errors) + def _initialize_context(self, context, **kwargs): """ Initialize the context attribute. diff --git a/tests/test_template.py b/tests/test_template.py index 9324a9d..dad8995 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -6,6 +6,7 @@ Unit tests of template.py. """ import codecs +import sys import unittest from pystache import template @@ -55,6 +56,92 @@ class TemplateTestCase(unittest.TestCase): template = Template(escape=escape) self.assertEquals(template.escape("bar"), "foobar") + def test_init__default_encoding__default(self): + """ + Check the default value. + + """ + template = Template() + self.assertEquals(template.default_encoding, sys.getdefaultencoding()) + + def test_init__default_encoding(self): + """ + Check that the constructor sets the attribute correctly. + + """ + template = Template(default_encoding="foo") + self.assertEquals(template.default_encoding, "foo") + + def test_init__decode_errors__default(self): + """ + Check the default value. + + """ + template = Template() + self.assertEquals(template.decode_errors, 'strict') + + def test_init__decode_errors(self): + """ + Check that the constructor sets the attribute correctly. + + """ + template = Template(decode_errors="foo") + self.assertEquals(template.decode_errors, "foo") + + def test_literal(self): + template = Template() + actual = template.literal("abc") + self.assertEquals(actual, "abc") + self.assertEquals(type(actual), unicode) + + def test_literal__default_encoding(self): + template = Template() + template.default_encoding = "utf-8" + actual = template.literal("é") + self.assertEquals(actual, u"é") + + def test_literal__default_encoding__error(self): + template = Template() + template.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, template.literal, "é") + + def test_literal__decode_errors(self): + template = Template() + template.default_encoding = "ascii" + s = "é" + + template.decode_errors = "strict" + self.assertRaises(UnicodeDecodeError, template.literal, s) + + template.decode_errors = "replace" + actual = template.literal(s) + # U+FFFD is the official Unicode replacement character. + self.assertEquals(actual, u'\ufffd\ufffd') + + def test_literal__with_markupsafe(self): + if not self._was_markupsafe_imported(): + # Then we cannot test this case. + return + self._restore_markupsafe() + + _template = Template() + _template.default_encoding = "utf_8" + + # Check the standard case. + actual = _template.literal("abc") + self.assertEquals(actual, "abc") + self.assertEquals(type(actual), template.markupsafe.Markup) + + s = "é" + # Check that markupsafe respects default_encoding. + self.assertEquals(_template.literal(s), u"é") + _template.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, _template.literal, s) + + # Check that markupsafe respects decode_errors. + _template.decode_errors = "replace" + self.assertEquals(_template.literal(s), u'\ufffd\ufffd') + def test_render__unicode(self): template = Template(u'foo') actual = template.render() -- cgit v1.2.1 From b445ee65f7761b58feebffee6c480b712c6a1620 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 20:16:05 -0800 Subject: More to finish issue #55. --- pystache/template.py | 27 +++++++++++++++++------ tests/test_template.py | 58 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 9a150f1..af8d595 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -128,9 +128,12 @@ class Template(object): def _unicode_and_escape(self, s): if not isinstance(s, unicode): - s = unicode(s) + s = self.unicode(s) return self.escape(s) + def unicode(self, s): + return unicode(s, self.default_encoding, self.decode_errors) + def escape(self, u): """ Escape a unicode string, and return it. @@ -142,7 +145,6 @@ class Template(object): """ pass - def literal(self, s): """ Convert the given string to a unicode string, without escaping it. @@ -154,7 +156,7 @@ class Template(object): markupsafe.Markup (which subclasses unicode). """ - return self._literal(s, self.default_encoding, self.decode_errors) + return self._literal(self.unicode(s)) def _initialize_context(self, context, **kwargs): """ @@ -174,7 +176,6 @@ class Template(object): self.context = context - def _compile_regexps(self): """ Compile and set the regular expression attributes. @@ -271,7 +272,8 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - template = Template(template, load_template=self.load_template, escape=self.escape) + template = Template(template, load_template=self.load_template, escape=self.escape, + default_encoding=self.default_encoding, decode_errors=self.decode_errors) out = template.render(self.context) self.context.pop() @@ -287,6 +289,10 @@ class Template(object): @modifiers.set(None) def _render_tag(self, tag_name): + """ + Return the value of a variable as an escaped unicode string. + + """ raw = self.context.get(tag_name, '') # For methods with no return value @@ -301,6 +307,14 @@ class Template(object): else: return '' + # If we don't first convert to a string type, the call to self._unicode_and_escape() + # will yield an error like the following: + # + # TypeError: coercing to Unicode: need string or buffer, ... found + # + if not isinstance(raw, basestring): + raw = str(raw) + return self._unicode_and_escape(raw) @modifiers.set('!') @@ -310,7 +324,8 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.load_template(template_name) - template = Template(markup, load_template=self.load_template, escape=self.escape) + template = Template(markup, load_template=self.load_template, escape=self.escape, + default_encoding=self.default_encoding, decode_errors=self.decode_errors) return template.render(self.context) @modifiers.set('=') diff --git a/tests/test_template.py b/tests/test_template.py index dad8995..9fd327d 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -88,35 +88,33 @@ class TemplateTestCase(unittest.TestCase): template = Template(decode_errors="foo") self.assertEquals(template.decode_errors, "foo") - def test_literal(self): + def test_unicode(self): template = Template() actual = template.literal("abc") self.assertEquals(actual, "abc") self.assertEquals(type(actual), unicode) - def test_literal__default_encoding(self): + def test_unicode__default_encoding(self): template = Template() - template.default_encoding = "utf-8" - actual = template.literal("é") - self.assertEquals(actual, u"é") + s = "é" - def test_literal__default_encoding__error(self): - template = Template() template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, template.literal, "é") + self.assertRaises(UnicodeDecodeError, template.unicode, s) + + template.default_encoding = "utf-8" + self.assertEquals(template.unicode(s), u"é") - def test_literal__decode_errors(self): + def test_unicode__decode_errors(self): template = Template() - template.default_encoding = "ascii" s = "é" + template.default_encoding = "ascii" template.decode_errors = "strict" - self.assertRaises(UnicodeDecodeError, template.literal, s) + self.assertRaises(UnicodeDecodeError, template.unicode, s) template.decode_errors = "replace" - actual = template.literal(s) # U+FFFD is the official Unicode replacement character. - self.assertEquals(actual, u'\ufffd\ufffd') + self.assertEquals(template.unicode(s), u'\ufffd\ufffd') def test_literal__with_markupsafe(self): if not self._was_markupsafe_imported(): @@ -295,3 +293,37 @@ class TemplateTestCase(unittest.TestCase): template = Template("{{#list}}{{name}}: {{greeting}}; {{/list}}") self.assertEquals(template.render(context), "Al: Hi; Bo: Hi; ") + + def test_render__encoding_in_context_value(self): + template = Template('{{test}}') + context = {'test': "déf"} + + template.decode_errors = 'ignore' + template.default_encoding = 'ascii' + self.assertEquals(template.render(context), "df") + + template.default_encoding = 'utf_8' + self.assertEquals(template.render(context), u"déf") + + def test_render__encoding_in_section_context_value(self): + template = Template('{{#test}}{{foo}}{{/test}}') + context = {'test': {'foo': "déf"}} + + template.decode_errors = 'ignore' + template.default_encoding = 'ascii' + self.assertEquals(template.render(context), "df") + + template.default_encoding = 'utf_8' + self.assertEquals(template.render(context), u"déf") + + def test_render__encoding_in_partial_context_value(self): + load_template = lambda x: "{{foo}}" + template = Template('{{>partial}}', load_template=load_template) + context = {'foo': "déf"} + + template.decode_errors = 'ignore' + template.default_encoding = 'ascii' + self.assertEquals(template.render(context), "df") + + template.default_encoding = 'utf_8' + self.assertEquals(template.render(context), u"déf") -- cgit v1.2.1 From 6409c25dae1d4c065834dd1233e1bf2f63058713 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 20:48:20 -0800 Subject: Fixed issue #57: non-unicode, non-ascii templates in Template.render() --- pystache/template.py | 18 ++++++++++++++---- tests/test_template.py | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index af8d595..ed5668c 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -197,6 +197,12 @@ class Template(object): self.tag_re = re.compile(tag) def _render(self, template): + """ + Arguments: + + template: a unicode template string. + + """ output = [] while True: @@ -350,11 +356,11 @@ class Template(object): def render(self, context=None, **kwargs): """ - Return the template rendered using the current context. + Return the template rendered using the given context. The return value is a unicode string, unless the output_encoding - attribute is not None, in which case the return value has type str - and is encoded using that encoding. + attribute has been set to a non-None value, in which case the + return value has type str and is encoded using that encoding. Arguments: @@ -366,7 +372,11 @@ class Template(object): """ self._initialize_context(context, **kwargs) - result = self._render(self.template) + template = self.template + if not isinstance(template, unicode): + template = self.unicode(template) + + result = self._render(template) if self.output_encoding is not None: result = result.encode(self.output_encoding) diff --git a/tests/test_template.py b/tests/test_template.py index 9fd327d..dc3c2f6 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -149,7 +149,7 @@ class TemplateTestCase(unittest.TestCase): def test_render__str(self): template = Template('foo') actual = template.render() - self.assertTrue(isinstance(actual, str)) + self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, 'foo') def test_render__non_ascii_character(self): @@ -327,3 +327,18 @@ class TemplateTestCase(unittest.TestCase): template.default_encoding = 'utf_8' self.assertEquals(template.render(context), u"déf") + + def test_render__nonascii_template(self): + """ + Test passing a non-unicode template with non-ascii characters. + + """ + template = Template("déf", output_encoding="utf-8") + + # Check that decode_errors and default_encoding are both respected. + template.decode_errors = 'ignore' + template.default_encoding = 'ascii' + self.assertEquals(template.render(), "df") + + template.default_encoding = 'utf_8' + self.assertEquals(template.render(), "déf") -- cgit v1.2.1 From 04da344dd75dec49ea2aa41dd27b9939fe99837a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 21:00:47 -0800 Subject: Updated docstrings for issue #57. --- pystache/template.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ed5668c..bb7f915 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -68,8 +68,9 @@ class Template(object): Arguments: - template: a template string as a unicode string. Behavior is - undefined if the string has type str. + template: a template string that is either unicode, or of type + str and encoded using the encoding named by the default_encoding + keyword argument. load_template: the function for loading partials. The function should accept a single template_name parameter and return a template as @@ -362,6 +363,10 @@ class Template(object): attribute has been set to a non-None value, in which case the return value has type str and is encoded using that encoding. + If the template string is not unicode, it is first converted to + unicode using the default_encoding and decode_errors attributes. + See the Template constructor's docstring for more information. + Arguments: context: a dictionary, Context, or object (e.g. a View instance). -- cgit v1.2.1 From dfeaac6d32bdbce6c739bdd3597ebdd6b81c6d36 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 21:04:07 -0800 Subject: Updated history notes for issue #57. --- HISTORY.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 5e7bc96..f234b3a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,9 @@ Features: * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] * Markupsafe can now be disabled after import. [cjerdonek] +* Template class can now handle non-ascii characters in non-unicode strings. + Added default_encoding and decode_errors to Template constructor arguments. + [cjerdonek] API changes: -- cgit v1.2.1 From d29197174de6d3148f6d3cd10499d485b82e03c7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 21:07:13 -0800 Subject: Updated history notes for issue #56. --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index f234b3a..e8c0fbb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,10 +6,10 @@ Next Release (version TBD) Features: -* Support for disabling HTML escape in Template and View classes. [cjerdonek] * A custom template loader can now be passed to a View. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] * Markupsafe can now be disabled after import. [cjerdonek] +* Custom escape function can now be passed to Template constructor. [cjerdonek] * Template class can now handle non-ascii characters in non-unicode strings. Added default_encoding and decode_errors to Template constructor arguments. [cjerdonek] -- cgit v1.2.1 From 984e5f5c149e3f768864089af279b1b0fbecc860 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Dec 2011 23:27:19 -0800 Subject: Template.render() no longer creates new Template instances while rendering. --- pystache/template.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index bb7f915..f195025 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -278,11 +278,7 @@ class Template(object): def _render_dictionary(self, template, context): self.context.push(context) - - template = Template(template, load_template=self.load_template, escape=self.escape, - default_encoding=self.default_encoding, decode_errors=self.decode_errors) - out = template.render(self.context) - + out = self._render(template) self.context.pop() return out @@ -331,9 +327,7 @@ class Template(object): @modifiers.set('>') def _render_partial(self, template_name): markup = self.load_template(template_name) - template = Template(markup, load_template=self.load_template, escape=self.escape, - default_encoding=self.default_encoding, decode_errors=self.decode_errors) - return template.render(self.context) + return self._render(markup) @modifiers.set('=') def _change_delimiter(self, tag_name): -- cgit v1.2.1 From 01d5b389dcd93e27cd2c5656b0e116c185b01ddb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 08:44:03 -0800 Subject: Split off some of the Template class into a RenderEngine class (issue #58). --- pystache/renderengine.py | 257 +++++++++++++++++++++++++++++++++++++++++++++++ pystache/template.py | 225 ++--------------------------------------- 2 files changed, 266 insertions(+), 216 deletions(-) create mode 100644 pystache/renderengine.py diff --git a/pystache/renderengine.py b/pystache/renderengine.py new file mode 100644 index 0000000..97907b0 --- /dev/null +++ b/pystache/renderengine.py @@ -0,0 +1,257 @@ +# coding: utf-8 + +""" +Defines a class responsible for rendering logic. + +""" + +import re + + +try: + # The collections.Callable class is not available until Python 2.6. + import collections.Callable + def check_callable(it): + return isinstance(it, collections.Callable) +except ImportError: + def check_callable(it): + return hasattr(it, '__call__') + + +class Modifiers(dict): + + """Dictionary with a decorator for assigning functions to keys.""" + + def set(self, key): + """ + Return a decorator that assigns the given function to the given key. + + >>> modifiers = {} + >>> @modifiers.set('P') + ... def render_tongue(self, tag_name=None, context=None): + ... return ":P %s" % tag_name + >>> modifiers + {'P': } + + """ + def decorate(func): + self[key] = func + return func + return decorate + + +class RenderEngine(object): + + """ + Provides a render() method. + + This class is meant only for internal use by the Template class. + + """ + tag_re = None + otag = '{{' + ctag = '}}' + + modifiers = Modifiers() + + def __init__(self, load_template=None, literal=None, escape=None): + """ + Arguments: + + escape: a function that takes a unicode or str string, + converts it to unicode, and escapes and returns it. + + literal: a function that converts a unicode or str string + to unicode without escaping, and returns it. + + """ + self.escape = escape + self.literal = literal + self.load_template = load_template + + def render(self, template, context): + """ + Arguments: + + template: a unicode template string. + context: a Context instance. + + """ + self.context = context + + self._compile_regexps() + + return self._render(template) + + def _compile_regexps(self): + """ + Compile and set the regular expression attributes. + + This method uses the current values for the otag and ctag attributes. + + """ + tags = { + 'otag': re.escape(self.otag), + 'ctag': re.escape(self.ctag) + } + + # The section contents include white space to comply with the spec's + # requirement that sections not alter surrounding whitespace. + section = r"%(otag)s([#|^])([^\}]*)%(ctag)s(.+?)%(otag)s/\2%(ctag)s" % tags + self.section_re = re.compile(section, re.M|re.S) + + tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" % tags + self.tag_re = re.compile(tag) + + def _render_tags(self, template): + output = [] + + while True: + parts = self.tag_re.split(template, maxsplit=1) + output.append(parts[0]) + + if len(parts) < 2: + # Then there was no match. + break + + start, tag_type, tag_name, template = parts + + tag_name = tag_name.strip() + func = self.modifiers[tag_type] + tag_value = func(self, tag_name) + + # Appending the tag value to the output prevents treating the + # value as a template string (bug: issue #44). + output.append(tag_value) + + output = "".join(output) + return output + + def _render_dictionary(self, template, context): + self.context.push(context) + out = self._render(template) + self.context.pop() + + return out + + def _render_list(self, template, listing): + insides = [] + for item in listing: + insides.append(self._render_dictionary(template, item)) + + return ''.join(insides) + + @modifiers.set(None) + def _render_tag(self, tag_name): + """ + Return the value of a variable as an escaped unicode string. + + """ + raw = self.context.get(tag_name, '') + + # For methods with no return value + # + # We use "==" rather than "is" to compare integers, as using "is" relies + # on an implementation detail of CPython. The test about rendering + # zeroes failed while using PyPy when using "is". + # See issue #34: https://github.com/defunkt/pystache/issues/34 + if not raw and raw != 0: + if tag_name == '.': + raw = self.context.top() + else: + return '' + + # If we don't first convert to a string type, the call to self._unicode_and_escape() + # will yield an error like the following: + # + # TypeError: coercing to Unicode: need string or buffer, ... found + # + if not isinstance(raw, basestring): + raw = str(raw) + + return self.escape(raw) + + @modifiers.set('!') + def _render_comment(self, tag_name): + return '' + + @modifiers.set('>') + def _render_partial(self, template_name): + markup = self.load_template(template_name) + return self._render(markup) + + @modifiers.set('=') + def _change_delimiter(self, tag_name): + """ + Change the current delimiter. + + """ + self.otag, self.ctag = tag_name.split(' ') + self._compile_regexps() + + return '' + + @modifiers.set('{') + @modifiers.set('&') + def render_unescaped(self, tag_name): + """ + Render a tag without escaping it. + + """ + return self.literal(self.context.get(tag_name, '')) + + def _render(self, template): + """ + Arguments: + + template: a unicode template string. + + """ + output = [] + + while True: + parts = self.section_re.split(template, maxsplit=1) + + start = self._render_tags(parts[0]) + output.append(start) + + if len(parts) < 2: + # Then there was no match. + break + + section_type, section_key, section_contents, template = parts[1:] + + section_key = section_key.strip() + section_value = self.context.get(section_key, None) + + rendered = '' + + # Callable + if section_value and check_callable(section_value): + rendered = section_value(section_contents) + + # Dictionary + elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): + if section_type != '^': + rendered = self._render_dictionary(section_contents, section_value) + + # Lists + elif section_value and hasattr(section_value, '__iter__'): + if section_type != '^': + rendered = self._render_list(section_contents, section_value) + + # Other objects + elif section_value and isinstance(section_value, object): + if section_type != '^': + rendered = self._render_dictionary(section_contents, section_value) + + # Falsey and Negated or Truthy and Not Negated + elif (not section_value and section_type == '^') or (section_value and section_type != '^'): + rendered = self._render_dictionary(section_contents, section_value) + + # Render template prior to section too + output.append(rendered) + + output = "".join(output) + return output + diff --git a/pystache/template.py b/pystache/template.py index f195025..a50bbe8 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -12,6 +12,7 @@ import sys from .context import Context from .loader import Loader +from .renderengine import RenderEngine markupsafe = None @@ -21,46 +22,8 @@ except ImportError: pass -try: - # The collections.Callable class is not available until Python 2.6. - import collections.Callable - def check_callable(it): - return isinstance(it, collections.Callable) -except ImportError: - def check_callable(it): - return hasattr(it, '__call__') - - -class Modifiers(dict): - - """Dictionary with a decorator for assigning functions to keys.""" - - def set(self, key): - """ - Return a decorator that assigns the given function to the given key. - - >>> modifiers = {} - >>> @modifiers.set('P') - ... def render_tongue(self, tag_name=None, context=None): - ... return ":P %s" % tag_name - >>> modifiers - {'P': } - - """ - def decorate(func): - self[key] = func - return func - return decorate - - class Template(object): - tag_re = None - otag = '{{' - ctag = '}}' - - modifiers = Modifiers() - def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, default_encoding=None, decode_errors='strict'): """ @@ -125,8 +88,6 @@ class Template(object): self.output_encoding = output_encoding self.template = template - self._compile_regexps() - def _unicode_and_escape(self, s): if not isinstance(s, unicode): s = self.unicode(s) @@ -159,7 +120,7 @@ class Template(object): """ return self._literal(self.unicode(s)) - def _initialize_context(self, context, **kwargs): + def _make_context(self, context, **kwargs): """ Initialize the context attribute. @@ -175,179 +136,7 @@ class Template(object): if kwargs: context.push(kwargs) - self.context = context - - def _compile_regexps(self): - """ - Compile and set the regular expression attributes. - - This method uses the current values for the otag and ctag attributes. - - """ - tags = { - 'otag': re.escape(self.otag), - 'ctag': re.escape(self.ctag) - } - - # The section contents include white space to comply with the spec's - # requirement that sections not alter surrounding whitespace. - section = r"%(otag)s([#|^])([^\}]*)%(ctag)s(.+?)%(otag)s/\2%(ctag)s" % tags - self.section_re = re.compile(section, re.M|re.S) - - tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" % tags - self.tag_re = re.compile(tag) - - def _render(self, template): - """ - Arguments: - - template: a unicode template string. - - """ - output = [] - - while True: - parts = self.section_re.split(template, maxsplit=1) - - start = self._render_tags(parts[0]) - output.append(start) - - if len(parts) < 2: - # Then there was no match. - break - - section_type, section_key, section_contents, template = parts[1:] - - section_key = section_key.strip() - section_value = self.context.get(section_key, None) - - rendered = '' - - # Callable - if section_value and check_callable(section_value): - rendered = section_value(section_contents) - - # Dictionary - elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): - if section_type != '^': - rendered = self._render_dictionary(section_contents, section_value) - - # Lists - elif section_value and hasattr(section_value, '__iter__'): - if section_type != '^': - rendered = self._render_list(section_contents, section_value) - - # Other objects - elif section_value and isinstance(section_value, object): - if section_type != '^': - rendered = self._render_dictionary(section_contents, section_value) - - # Falsey and Negated or Truthy and Not Negated - elif (not section_value and section_type == '^') or (section_value and section_type != '^'): - rendered = self._render_dictionary(section_contents, section_value) - - # Render template prior to section too - output.append(rendered) - - output = "".join(output) - return output - - def _render_tags(self, template): - output = [] - - while True: - parts = self.tag_re.split(template, maxsplit=1) - output.append(parts[0]) - - if len(parts) < 2: - # Then there was no match. - break - - start, tag_type, tag_name, template = parts - - tag_name = tag_name.strip() - func = self.modifiers[tag_type] - tag_value = func(self, tag_name) - - # Appending the tag value to the output prevents treating the - # value as a template string (bug: issue #44). - output.append(tag_value) - - output = "".join(output) - return output - - def _render_dictionary(self, template, context): - self.context.push(context) - out = self._render(template) - self.context.pop() - - return out - - def _render_list(self, template, listing): - insides = [] - for item in listing: - insides.append(self._render_dictionary(template, item)) - - return ''.join(insides) - - @modifiers.set(None) - def _render_tag(self, tag_name): - """ - Return the value of a variable as an escaped unicode string. - - """ - raw = self.context.get(tag_name, '') - - # For methods with no return value - # - # We use "==" rather than "is" to compare integers, as using "is" relies - # on an implementation detail of CPython. The test about rendering - # zeroes failed while using PyPy when using "is". - # See issue #34: https://github.com/defunkt/pystache/issues/34 - if not raw and raw != 0: - if tag_name == '.': - raw = self.context.top() - else: - return '' - - # If we don't first convert to a string type, the call to self._unicode_and_escape() - # will yield an error like the following: - # - # TypeError: coercing to Unicode: need string or buffer, ... found - # - if not isinstance(raw, basestring): - raw = str(raw) - - return self._unicode_and_escape(raw) - - @modifiers.set('!') - def _render_comment(self, tag_name): - return '' - - @modifiers.set('>') - def _render_partial(self, template_name): - markup = self.load_template(template_name) - return self._render(markup) - - @modifiers.set('=') - def _change_delimiter(self, tag_name): - """ - Change the current delimiter. - - """ - self.otag, self.ctag = tag_name.split(' ') - self._compile_regexps() - - return '' - - @modifiers.set('{') - @modifiers.set('&') - def render_unescaped(self, tag_name): - """ - Render a tag without escaping it. - - """ - return self.literal(self.context.get(tag_name, '')) + return context def render(self, context=None, **kwargs): """ @@ -369,13 +158,17 @@ class Template(object): These values take precedence over the context on any key conflicts. """ - self._initialize_context(context, **kwargs) + context = self._make_context(context, **kwargs) template = self.template if not isinstance(template, unicode): template = self.unicode(template) - result = self._render(template) + engine = RenderEngine(load_template=self.load_template, + literal=self.literal, + escape=self._unicode_and_escape) + + result = engine.render(template, context) if self.output_encoding is not None: result = result.encode(self.output_encoding) -- cgit v1.2.1 From 9637ef6c1be7833443031590f9457cc01da5e9ad Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:14 -0800 Subject: Fixed some import statements. --- pystache/renderengine.py | 1 + pystache/template.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 97907b0..c94260f 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -5,6 +5,7 @@ Defines a class responsible for rendering logic. """ +import collections import re diff --git a/pystache/template.py b/pystache/template.py index a50bbe8..ef16750 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -6,8 +6,6 @@ This module provides a Template class. """ import cgi -import collections -import re import sys from .context import Context -- cgit v1.2.1 From 5f73682d81e1ce59a49aad7337d9a9e6d88dac92 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:15 -0800 Subject: Added a unit test to test that we're checking for markupsafe correctly. --- tests/test_template.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_template.py b/tests/test_template.py index dc3c2f6..3dfa89c 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -38,6 +38,19 @@ class TemplateTestCase(unittest.TestCase): """ template.markupsafe = self.original_markupsafe + def test__was_markupsafe_imported(self): + """ + Test that our helper function works. + + """ + markupsafe = None + try: + import markupsafe + except: + pass + + self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) + def test_init__escape__default_without_markupsafe(self): template = Template() self.assertEquals(template.escape(">'"), ">'") -- cgit v1.2.1 From 35cf712f4b7937e616e71a3fbc2cc58e59dd14ac Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:15 -0800 Subject: Stubbed out a unit test for the new RenderEngine class. --- tests/test_renderengine.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_renderengine.py diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py new file mode 100644 index 0000000..a4c50e1 --- /dev/null +++ b/tests/test_renderengine.py @@ -0,0 +1,24 @@ +# coding: utf-8 + +""" +Unit tests of renderengine.py. + +""" + +import cgi +import unittest + +from pystache.context import Context +from pystache.renderengine import RenderEngine + + +class RenderEngineTestCase(unittest.TestCase): + + """Test the RenderEngine class.""" + + def test_render(self): + escape = lambda s: cgi.escape(unicode(s)) + engine = RenderEngine(literal=unicode, escape=escape) + context = Context({'person': 'Mom'}) + actual = engine.render('Hi {{person}}', context) + self.assertEquals(actual, 'Hi Mom') -- cgit v1.2.1 From 1ab8cf8c8d1eed6a5e786c4b942591d936844e8d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:15 -0800 Subject: Refactored the initial RenderEngine unit tests to use helper methods. --- tests/test_renderengine.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index a4c50e1..4f2a601 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -16,9 +16,36 @@ class RenderEngineTestCase(unittest.TestCase): """Test the RenderEngine class.""" + def _engine(self, partials=None): + """ + Create and return a default RenderEngine for testing. + + """ + load_template = None + to_unicode = unicode + + escape = lambda s: cgi.escape(to_unicode(s)) + literal = to_unicode + + if partials is not None: + load_template = lambda key: partials[key] + + engine = RenderEngine(literal=literal, escape=escape, load_template=load_template) + return engine + + def _assert_render(self, engine, expected, template, *context): + if engine is None: + engine = self._engine() + + context = Context(*context) + + actual = engine.render(template, context) + self.assertEquals(actual, expected) + def test_render(self): - escape = lambda s: cgi.escape(unicode(s)) - engine = RenderEngine(literal=unicode, escape=escape) - context = Context({'person': 'Mom'}) - actual = engine.render('Hi {{person}}', context) - self.assertEquals(actual, 'Hi Mom') + self._assert_render(None, 'Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) + + def test_render_with_partial(self): + partials = {'partial': "{{person}}"} + engine = self._engine(partials) + self._assert_render(engine, 'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}) -- cgit v1.2.1 From 28233892000093bf8b5ed1dd2207b0bcebfd0e1a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:16 -0800 Subject: Made the engine argument a keyword argument. --- tests/test_renderengine.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 4f2a601..f9ddafb 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -33,19 +33,18 @@ class RenderEngineTestCase(unittest.TestCase): engine = RenderEngine(literal=literal, escape=escape, load_template=load_template) return engine - def _assert_render(self, engine, expected, template, *context): - if engine is None: - engine = self._engine() - + def _assert_render(self, expected, template, *context, **kwargs): + engine = kwargs['engine'] if kwargs else self._engine() context = Context(*context) actual = engine.render(template, context) + self.assertEquals(actual, expected) def test_render(self): - self._assert_render(None, 'Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) + self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) def test_render_with_partial(self): partials = {'partial': "{{person}}"} engine = self._engine(partials) - self._assert_render(engine, 'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}) + self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) -- cgit v1.2.1 From 111cf43601e5f56f6862136829f0ed872040304a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:16 -0800 Subject: Added Template._make_render_engine() and tests for it. --- pystache/template.py | 15 +++++++++++---- tests/test_template.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ef16750..1754243 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -136,6 +136,16 @@ class Template(object): return context + def _make_render_engine(self): + """ + Return a RenderEngine instance for rendering. + + """ + engine = RenderEngine(load_template=self.load_template, + literal=self.literal, + escape=self._unicode_and_escape) + return engine + def render(self, context=None, **kwargs): """ Return the template rendered using the given context. @@ -156,16 +166,13 @@ class Template(object): These values take precedence over the context on any key conflicts. """ + engine = self._make_render_engine() context = self._make_context(context, **kwargs) template = self.template if not isinstance(template, unicode): template = self.unicode(template) - engine = RenderEngine(load_template=self.load_template, - literal=self.literal, - escape=self._unicode_and_escape) - result = engine.render(template, context) if self.output_encoding is not None: diff --git a/tests/test_template.py b/tests/test_template.py index 3dfa89c..c40597a 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -355,3 +355,46 @@ class TemplateTestCase(unittest.TestCase): template.default_encoding = 'utf_8' self.assertEquals(template.render(), "déf") + + # By testing that Template.render() constructs the RenderEngine instance + # correctly, we no longer need to test the rendering code paths through + # the Template. We can test rendering paths through only the RenderEngine + # for the same amount of code coverage. + def test_make_render_engine__load_template(self): + """ + Test that _make_render_engine() passes the right load_template. + + """ + template = Template() + template.load_template = "foo" # in real life, this would be a function. + + engine = template._make_render_engine() + self.assertEquals(engine.load_template, "foo") + + def test_make_render_engine__literal(self): + """ + Test that _make_render_engine() passes the right literal. + + """ + template = Template() + template.literal = "foo" # in real life, this would be a function. + + engine = template._make_render_engine() + self.assertEquals(engine.literal, "foo") + + def test_make_render_engine__escape(self): + """ + Test that _make_render_engine() passes the right escape. + + """ + template = Template() + template.unicode = lambda s: s.upper() # a test version. + template.escape = lambda s: "**" + s # a test version. + + engine = template._make_render_engine() + escape = engine.escape + + self.assertEquals(escape(u"foo"), "**foo") + + # Test that escape converts str strings to unicode first. + self.assertEquals(escape("foo"), "**FOO") -- cgit v1.2.1 From 061c96b6146f142cdb5e957e7f7a7c97a5521353 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:16 -0800 Subject: Added a test for RenderEngine.__init__(). --- tests/test_renderengine.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index f9ddafb..2dce92a 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -41,6 +41,18 @@ class RenderEngineTestCase(unittest.TestCase): self.assertEquals(actual, expected) + def test_init(self): + """ + Test that __init__() stores all of the arguments correctly. + + """ + # In real-life, these arguments would be functions + engine = RenderEngine(load_template="load_template", literal="literal", escape="escape") + + self.assertEquals(engine.escape, "escape") + self.assertEquals(engine.literal, "literal") + self.assertEquals(engine.load_template, "load_template") + def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) -- cgit v1.2.1 From 12b0cad2ee5274946bec51f89cb0cd21f3f57fd4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:16 -0800 Subject: Added tests that RenderEngine.render() uses its attributes. --- tests/test_renderengine.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 2dce92a..6a8d8c3 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -56,6 +56,40 @@ class RenderEngineTestCase(unittest.TestCase): def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) + def test_render__load_template(self): + """ + Test that render() uses the load_template attribute. + + """ + engine = self._engine() + + partials = {'partial': "{{person}}"} + engine.load_template = lambda key: partials[key] + + self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) + + def test_render__literal(self): + """ + Test that render() uses the literal attribute. + + """ + engine = self._engine() + + engine.literal = lambda s: s.upper() + + self._assert_render('bar BAR', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) + + def test_render__escape(self): + """ + Test that render() uses the escape attribute. + + """ + engine = self._engine() + + engine.escape = lambda s: "**" + s + + self._assert_render('**bar bar', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) + def test_render_with_partial(self): partials = {'partial': "{{person}}"} engine = self._engine(partials) -- cgit v1.2.1 From 0ad0e95a16ba4037f99ffcdeae4afe7635d522a7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:16 -0800 Subject: Added a partials keyword to test_renderengine.py. --- tests/test_renderengine.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 6a8d8c3..58317d2 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -16,7 +16,7 @@ class RenderEngineTestCase(unittest.TestCase): """Test the RenderEngine class.""" - def _engine(self, partials=None): + def _engine(self): """ Create and return a default RenderEngine for testing. @@ -27,14 +27,16 @@ class RenderEngineTestCase(unittest.TestCase): escape = lambda s: cgi.escape(to_unicode(s)) literal = to_unicode - if partials is not None: - load_template = lambda key: partials[key] - - engine = RenderEngine(literal=literal, escape=escape, load_template=load_template) + engine = RenderEngine(literal=literal, escape=escape, load_template=None) return engine def _assert_render(self, expected, template, *context, **kwargs): - engine = kwargs['engine'] if kwargs else self._engine() + partials = kwargs.get('partials') + engine = kwargs.get('engine', self._engine()) + + if partials is not None: + engine.load_template = lambda key: partials[key] + context = Context(*context) actual = engine.render(template, context) @@ -62,10 +64,8 @@ class RenderEngineTestCase(unittest.TestCase): """ engine = self._engine() - partials = {'partial': "{{person}}"} engine.load_template = lambda key: partials[key] - self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) def test_render__literal(self): @@ -74,9 +74,7 @@ class RenderEngineTestCase(unittest.TestCase): """ engine = self._engine() - engine.literal = lambda s: s.upper() - self._assert_render('bar BAR', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) def test_render__escape(self): @@ -85,12 +83,9 @@ class RenderEngineTestCase(unittest.TestCase): """ engine = self._engine() - engine.escape = lambda s: "**" + s - self._assert_render('**bar bar', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) def test_render_with_partial(self): partials = {'partial': "{{person}}"} - engine = self._engine(partials) - self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) + self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) -- cgit v1.2.1 From b885d02963dbabacf16670b43eaebcaf524858f2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:17 -0800 Subject: Moved some tests from test_template to test_renderengine. --- tests/test_renderengine.py | 28 ++++++++++++++++++++++++++++ tests/test_template.py | 34 ---------------------------------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 58317d2..f424ba8 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -89,3 +89,31 @@ class RenderEngineTestCase(unittest.TestCase): def test_render_with_partial(self): partials = {'partial': "{{person}}"} self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) + + def test_render__section_context_values(self): + """ + Test that escape and literal work on context values in sections. + + """ + engine = self._engine() + engine.escape = lambda s: "**" + s + engine.literal = lambda s: s.upper() + + template = '{{#test}}{{foo}} {{{foo}}}{{/test}}' + context = {'test': {'foo': 'bar'}} + + self._assert_render('**bar BAR', template, context, engine=engine) + + def test_render__partial_context_values(self): + """ + Test that escape and literal work on context values in partials. + + """ + engine = self._engine() + engine.escape = lambda s: "**" + s + engine.literal = lambda s: s.upper() + + partials = {'partial': '{{foo}} {{{foo}}}'} + + self._assert_render('**bar BAR', '{{>partial}}', {'foo': 'bar'}, engine=engine, partials=partials) + diff --git a/tests/test_template.py b/tests/test_template.py index c40597a..0fff0b7 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -307,40 +307,6 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(template.render(context), "Al: Hi; Bo: Hi; ") - def test_render__encoding_in_context_value(self): - template = Template('{{test}}') - context = {'test': "déf"} - - template.decode_errors = 'ignore' - template.default_encoding = 'ascii' - self.assertEquals(template.render(context), "df") - - template.default_encoding = 'utf_8' - self.assertEquals(template.render(context), u"déf") - - def test_render__encoding_in_section_context_value(self): - template = Template('{{#test}}{{foo}}{{/test}}') - context = {'test': {'foo': "déf"}} - - template.decode_errors = 'ignore' - template.default_encoding = 'ascii' - self.assertEquals(template.render(context), "df") - - template.default_encoding = 'utf_8' - self.assertEquals(template.render(context), u"déf") - - def test_render__encoding_in_partial_context_value(self): - load_template = lambda x: "{{foo}}" - template = Template('{{>partial}}', load_template=load_template) - context = {'foo': "déf"} - - template.decode_errors = 'ignore' - template.default_encoding = 'ascii' - self.assertEquals(template.render(context), "df") - - template.default_encoding = 'utf_8' - self.assertEquals(template.render(context), u"déf") - def test_render__nonascii_template(self): """ Test passing a non-unicode template with non-ascii characters. -- cgit v1.2.1 From b05d6f85ceae56ce0b2e224d529e29274e846f40 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:17 -0800 Subject: Removed some redundant tests from test_template, and moved one to test_renderengine. --- tests/test_renderengine.py | 18 ++++++++++++++++ tests/test_template.py | 51 ---------------------------------------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index f424ba8..a51adfc 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -117,3 +117,21 @@ class RenderEngineTestCase(unittest.TestCase): self._assert_render('**bar BAR', '{{>partial}}', {'foo': 'bar'}, engine=engine, partials=partials) + def test_render__list_referencing_outer_context(self): + """ + Check that list items can access the parent context. + + For sections whose value is a list, check that items in the list + have access to the values inherited from the parent context + when rendering. + + """ + context = { + "list": [{"name": "Al"}, {"name": "Bo"}], + "greeting": "Hi", + } + + template = "{{#list}}{{name}}: {{greeting}}; {{/list}}" + + self._assert_render("Al: Hi; Bo: Hi; ", template, context) + diff --git a/tests/test_template.py b/tests/test_template.py index 0fff0b7..fcfc5b5 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -256,57 +256,6 @@ class TemplateTestCase(unittest.TestCase): actual = template.render(context) self.assertEquals(actual, '{{hi}} Mom') - def test_render__html_escape(self): - context = {'test': '1 < 2'} - template = Template('{{test}}') - - self.assertEquals(template.render(context), '1 < 2') - - def test_render__html_escape_disabled(self): - context = {'test': '1 < 2'} - template = Template('{{test}}') - - self.assertEquals(template.render(context), '1 < 2') - - template.escape = lambda s: s - self.assertEquals(template.render(context), '1 < 2') - - def test_render__html_escape_disabled_with_partial(self): - context = {'test': '1 < 2'} - load_template = lambda name: '{{test}}' - template = Template('{{>partial}}', load_template=load_template) - - self.assertEquals(template.render(context), '1 < 2') - - template.escape = lambda s: s - self.assertEquals(template.render(context), '1 < 2') - - def test_render__html_escape_disabled_with_non_false_value(self): - context = {'section': {'test': '1 < 2'}} - template = Template('{{#section}}{{test}}{{/section}}') - - self.assertEquals(template.render(context), '1 < 2') - - template.escape = lambda s: s - self.assertEquals(template.render(context), '1 < 2') - - def test_render__list_referencing_outer_context(self): - """ - Check that list items can access the parent context. - - For sections whose value is a list, check that items in the list - have access to the values inherited from the parent context - when rendering. - - """ - context = { - "list": [{"name": "Al"}, {"name": "Bo"}], - "greeting": "Hi", - } - template = Template("{{#list}}{{name}}: {{greeting}}; {{/list}}") - - self.assertEquals(template.render(context), "Al: Hi; Bo: Hi; ") - def test_render__nonascii_template(self): """ Test passing a non-unicode template with non-ascii characters. -- cgit v1.2.1 From 331f149e78bfce5c9c8f82e438979f310f917c77 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 18:23:17 -0800 Subject: Moved four tests from test_template to test_renderengine. --- tests/test_renderengine.py | 32 ++++++++++++++++++++++++++++++++ tests/test_template.py | 36 ------------------------------------ 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index a51adfc..6f75872 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -135,3 +135,35 @@ class RenderEngineTestCase(unittest.TestCase): self._assert_render("Al: Hi; Bo: Hi; ", template, context) + def test_render__tag_in_value(self): + """ + Context values should not be treated as templates (issue #44). + + """ + template = '{{test}}' + context = {'test': '{{hello}}'} + self._assert_render('{{hello}}', template, context) + + def test_render__section_in_value(self): + """ + Context values should not be treated as templates (issue #44). + + """ + template = '{{test}}' + context = {'test': '{{#hello}}'} + self._assert_render('{{#hello}}', template, context) + + def test_render__section__lambda(self): + template = '{{#test}}Mom{{/test}}' + context = {'test': (lambda text: 'Hi %s' % text)} + self._assert_render('Hi Mom', template, context) + + def test_render__section__lambda__tag_in_output(self): + """ + Check that callable output isn't treated as a template string (issue #46). + + """ + template = '{{#test}}Mom{{/test}}' + context = {'test': (lambda text: '{{hi}} %s' % text)} + self._assert_render('{{hi}} Mom', template, context) + diff --git a/tests/test_template.py b/tests/test_template.py index fcfc5b5..0944703 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -220,42 +220,6 @@ class TemplateTestCase(unittest.TestCase): self.assertTrue(isinstance(actual, str)) self.assertEquals(actual, 'Poincaré') - def test_render__tag_in_value(self): - """ - Context values should not be treated as templates (issue #44). - - """ - template = Template('{{test}}') - context = {'test': '{{hello}}'} - actual = template.render(context) - self.assertEquals(actual, '{{hello}}') - - def test_render__section_in_value(self): - """ - Context values should not be treated as templates (issue #44). - - """ - template = Template('{{test}}') - context = {'test': '{{#hello}}'} - actual = template.render(context) - self.assertEquals(actual, '{{#hello}}') - - def test_render__section__lambda(self): - template = Template('{{#test}}Mom{{/test}}') - context = {'test': (lambda text: 'Hi %s' % text)} - actual = template.render(context) - self.assertEquals(actual, 'Hi Mom') - - def test_render__section__lambda__tag_in_output(self): - """ - Check that callable output isn't treated as a template string (issue #46). - - """ - template = Template('{{#test}}Mom{{/test}}') - context = {'test': (lambda text: '{{hi}} %s' % text)} - actual = template.render(context) - self.assertEquals(actual, '{{hi}} Mom') - def test_render__nonascii_template(self): """ Test passing a non-unicode template with non-ascii characters. -- cgit v1.2.1 From 65e347a96d59297a594f2f6f7c6607626082dd75 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:02:29 -0800 Subject: Renamed template.py to renderer.py. --- pystache/commands.py | 2 +- pystache/init.py | 2 +- pystache/renderer.py | 181 ++++++++++++++++++++++++++++++++ pystache/template.py | 181 -------------------------------- pystache/view.py | 2 +- tests/test_pystache.py | 12 +-- tests/test_renderer.py | 279 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_template.py | 279 ------------------------------------------------- 8 files changed, 469 insertions(+), 469 deletions(-) create mode 100644 pystache/renderer.py delete mode 100644 pystache/template.py create mode 100644 tests/test_renderer.py delete mode 100644 tests/test_template.py diff --git a/pystache/commands.py b/pystache/commands.py index ac5be88..5326281 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -20,7 +20,7 @@ import sys # ValueError: Attempted relative import in non-package # from pystache.loader import Loader -from pystache.template import Template +from pystache.renderer import Template USAGE = """\ diff --git a/pystache/init.py b/pystache/init.py index 4366f69..48c8a2d 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,7 +5,7 @@ This module contains the initialization logic called by __init__.py. """ -from .template import Template +from .renderer import Template from .view import View from .loader import Loader diff --git a/pystache/renderer.py b/pystache/renderer.py new file mode 100644 index 0000000..1754243 --- /dev/null +++ b/pystache/renderer.py @@ -0,0 +1,181 @@ +# coding: utf-8 + +""" +This module provides a Template class. + +""" + +import cgi +import sys + +from .context import Context +from .loader import Loader +from .renderengine import RenderEngine + + +markupsafe = None +try: + import markupsafe +except ImportError: + pass + + +class Template(object): + + def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, + default_encoding=None, decode_errors='strict'): + """ + Construct a Template instance. + + Arguments: + + template: a template string that is either unicode, or of type + str and encoded using the encoding named by the default_encoding + keyword argument. + + load_template: the function for loading partials. The function should + accept a single template_name parameter and return a template as + a string. Defaults to the default Loader's load_template() method. + + output_encoding: the encoding to use when rendering to a string. + The argument should be the name of an encoding as a string, for + example "utf-8". See the render() method's documentation for more + information. + + escape: the function used to escape mustache variable values + when rendering a template. The function should accept a unicode + string and return an escaped string of the same type. It need + not handle strings of type `str` because this class will only + pass it unicode strings. The constructor assigns this escape + function to the constructed instance's Template.escape() method. + + The argument defaults to markupsafe.escape when markupsafe is + importable and cgi.escape otherwise. To disable escaping entirely, + one can pass `lambda s: s` as the escape function, for example. + + default_encoding: the name of the encoding to use when converting + to unicode any strings of type `str` encountered during the + rendering process. The name will be passed as the "encoding" + argument to the built-in function unicode(). Defaults to the + encoding name returned by sys.getdefaultencoding(). + + decode_errors: the string to pass as the "errors" argument to the + built-in function unicode() when converting to unicode any + strings of type `str` encountered during the rendering process. + Defaults to "strict". + + """ + if load_template is None: + loader = Loader() + load_template = loader.load_template + + if default_encoding is None: + default_encoding = sys.getdefaultencoding() + + if escape is None: + escape = markupsafe.escape if markupsafe else cgi.escape + + literal = markupsafe.Markup if markupsafe else unicode + + self._literal = literal + + self.decode_errors = decode_errors + self.default_encoding = default_encoding + self.escape = escape + self.load_template = load_template + self.output_encoding = output_encoding + self.template = template + + def _unicode_and_escape(self, s): + if not isinstance(s, unicode): + s = self.unicode(s) + return self.escape(s) + + def unicode(self, s): + return unicode(s, self.default_encoding, self.decode_errors) + + def escape(self, u): + """ + Escape a unicode string, and return it. + + This function is initialized as the escape function that was passed + to the Template class's constructor when this instance was + constructed. See the constructor docstring for more information. + + """ + pass + + def literal(self, s): + """ + Convert the given string to a unicode string, without escaping it. + + This function internally calls the built-in function unicode() and + passes it the default_encoding and decode_errors attributes for this + Template instance. If markupsafe was importable when loading this + module, this function returns an instance of the class + markupsafe.Markup (which subclasses unicode). + + """ + return self._literal(self.unicode(s)) + + def _make_context(self, context, **kwargs): + """ + Initialize the context attribute. + + """ + if context is None: + context = {} + + if isinstance(context, Context): + context = context.copy() + else: + context = Context(context) + + if kwargs: + context.push(kwargs) + + return context + + def _make_render_engine(self): + """ + Return a RenderEngine instance for rendering. + + """ + engine = RenderEngine(load_template=self.load_template, + literal=self.literal, + escape=self._unicode_and_escape) + return engine + + def render(self, context=None, **kwargs): + """ + Return the template rendered using the given context. + + The return value is a unicode string, unless the output_encoding + attribute has been set to a non-None value, in which case the + return value has type str and is encoded using that encoding. + + If the template string is not unicode, it is first converted to + unicode using the default_encoding and decode_errors attributes. + See the Template constructor's docstring for more information. + + Arguments: + + context: a dictionary, Context, or object (e.g. a View instance). + + **kwargs: additional key values to add to the context when rendering. + These values take precedence over the context on any key conflicts. + + """ + engine = self._make_render_engine() + context = self._make_context(context, **kwargs) + + template = self.template + if not isinstance(template, unicode): + template = self.unicode(template) + + result = engine.render(template, context) + + if self.output_encoding is not None: + result = result.encode(self.output_encoding) + + return result diff --git a/pystache/template.py b/pystache/template.py deleted file mode 100644 index 1754243..0000000 --- a/pystache/template.py +++ /dev/null @@ -1,181 +0,0 @@ -# coding: utf-8 - -""" -This module provides a Template class. - -""" - -import cgi -import sys - -from .context import Context -from .loader import Loader -from .renderengine import RenderEngine - - -markupsafe = None -try: - import markupsafe -except ImportError: - pass - - -class Template(object): - - def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, - default_encoding=None, decode_errors='strict'): - """ - Construct a Template instance. - - Arguments: - - template: a template string that is either unicode, or of type - str and encoded using the encoding named by the default_encoding - keyword argument. - - load_template: the function for loading partials. The function should - accept a single template_name parameter and return a template as - a string. Defaults to the default Loader's load_template() method. - - output_encoding: the encoding to use when rendering to a string. - The argument should be the name of an encoding as a string, for - example "utf-8". See the render() method's documentation for more - information. - - escape: the function used to escape mustache variable values - when rendering a template. The function should accept a unicode - string and return an escaped string of the same type. It need - not handle strings of type `str` because this class will only - pass it unicode strings. The constructor assigns this escape - function to the constructed instance's Template.escape() method. - - The argument defaults to markupsafe.escape when markupsafe is - importable and cgi.escape otherwise. To disable escaping entirely, - one can pass `lambda s: s` as the escape function, for example. - - default_encoding: the name of the encoding to use when converting - to unicode any strings of type `str` encountered during the - rendering process. The name will be passed as the "encoding" - argument to the built-in function unicode(). Defaults to the - encoding name returned by sys.getdefaultencoding(). - - decode_errors: the string to pass as the "errors" argument to the - built-in function unicode() when converting to unicode any - strings of type `str` encountered during the rendering process. - Defaults to "strict". - - """ - if load_template is None: - loader = Loader() - load_template = loader.load_template - - if default_encoding is None: - default_encoding = sys.getdefaultencoding() - - if escape is None: - escape = markupsafe.escape if markupsafe else cgi.escape - - literal = markupsafe.Markup if markupsafe else unicode - - self._literal = literal - - self.decode_errors = decode_errors - self.default_encoding = default_encoding - self.escape = escape - self.load_template = load_template - self.output_encoding = output_encoding - self.template = template - - def _unicode_and_escape(self, s): - if not isinstance(s, unicode): - s = self.unicode(s) - return self.escape(s) - - def unicode(self, s): - return unicode(s, self.default_encoding, self.decode_errors) - - def escape(self, u): - """ - Escape a unicode string, and return it. - - This function is initialized as the escape function that was passed - to the Template class's constructor when this instance was - constructed. See the constructor docstring for more information. - - """ - pass - - def literal(self, s): - """ - Convert the given string to a unicode string, without escaping it. - - This function internally calls the built-in function unicode() and - passes it the default_encoding and decode_errors attributes for this - Template instance. If markupsafe was importable when loading this - module, this function returns an instance of the class - markupsafe.Markup (which subclasses unicode). - - """ - return self._literal(self.unicode(s)) - - def _make_context(self, context, **kwargs): - """ - Initialize the context attribute. - - """ - if context is None: - context = {} - - if isinstance(context, Context): - context = context.copy() - else: - context = Context(context) - - if kwargs: - context.push(kwargs) - - return context - - def _make_render_engine(self): - """ - Return a RenderEngine instance for rendering. - - """ - engine = RenderEngine(load_template=self.load_template, - literal=self.literal, - escape=self._unicode_and_escape) - return engine - - def render(self, context=None, **kwargs): - """ - Return the template rendered using the given context. - - The return value is a unicode string, unless the output_encoding - attribute has been set to a non-None value, in which case the - return value has type str and is encoded using that encoding. - - If the template string is not unicode, it is first converted to - unicode using the default_encoding and decode_errors attributes. - See the Template constructor's docstring for more information. - - Arguments: - - context: a dictionary, Context, or object (e.g. a View instance). - - **kwargs: additional key values to add to the context when rendering. - These values take precedence over the context on any key conflicts. - - """ - engine = self._make_render_engine() - context = self._make_context(context, **kwargs) - - template = self.template - if not isinstance(template, unicode): - template = self.unicode(template) - - result = engine.render(template, context) - - if self.output_encoding is not None: - result = result.encode(self.output_encoding) - - return result diff --git a/pystache/view.py b/pystache/view.py index fa8ac47..452cb30 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -10,7 +10,7 @@ from types import UnboundMethodType from .context import Context from .loader import Loader -from .template import Template +from .renderer import Template class View(object): diff --git a/tests/test_pystache.py b/tests/test_pystache.py index 31602ae..012f45f 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -2,7 +2,7 @@ import unittest import pystache -from pystache import template +from pystache import renderer class PystacheTests(object): @@ -114,16 +114,16 @@ class PystacheWithoutMarkupsafeTests(PystacheTests, unittest.TestCase): """Test pystache without markupsafe enabled.""" def setUp(self): - self.original_markupsafe = template.markupsafe - template.markupsafe = None + self.original_markupsafe = renderer.markupsafe + renderer.markupsafe = None def tearDown(self): - template.markupsafe = self.original_markupsafe + renderer.markupsafe = self.original_markupsafe # If markupsafe is available, then run the same tests again but without # disabling markupsafe. -_BaseClass = unittest.TestCase if template.markupsafe else object +_BaseClass = unittest.TestCase if renderer.markupsafe else object class PystacheWithMarkupsafeTests(PystacheTests, _BaseClass): """Test pystache with markupsafe enabled.""" @@ -132,5 +132,5 @@ class PystacheWithMarkupsafeTests(PystacheTests, _BaseClass): non_strings_expected = """(123 & ['something'])(chris & 0.9)""" def test_markupsafe_available(self): - self.assertTrue(template.markupsafe, "markupsafe isn't available. " + self.assertTrue(renderer.markupsafe, "markupsafe isn't available. " "The with-markupsafe tests shouldn't be running.") diff --git a/tests/test_renderer.py b/tests/test_renderer.py new file mode 100644 index 0000000..e17ea63 --- /dev/null +++ b/tests/test_renderer.py @@ -0,0 +1,279 @@ +# coding: utf-8 + +""" +Unit tests of template.py. + +""" + +import codecs +import sys +import unittest + +from pystache import renderer +from pystache.renderer import Template + + +class TemplateTestCase(unittest.TestCase): + + """Test the Template class.""" + + def setUp(self): + """ + Disable markupsafe. + + """ + self.original_markupsafe = renderer.markupsafe + renderer.markupsafe = None + + def tearDown(self): + self._restore_markupsafe() + + def _was_markupsafe_imported(self): + return bool(self.original_markupsafe) + + def _restore_markupsafe(self): + """ + Restore markupsafe to its original state. + + """ + renderer.markupsafe = self.original_markupsafe + + def test__was_markupsafe_imported(self): + """ + Test that our helper function works. + + """ + markupsafe = None + try: + import markupsafe + except: + pass + + self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) + + def test_init__escape__default_without_markupsafe(self): + template = Template() + self.assertEquals(template.escape(">'"), ">'") + + def test_init__escape__default_with_markupsafe(self): + if not self._was_markupsafe_imported(): + # Then we cannot test this case. + return + self._restore_markupsafe() + + template = Template() + self.assertEquals(template.escape(">'"), ">'") + + def test_init__escape(self): + escape = lambda s: "foo" + s + template = Template(escape=escape) + self.assertEquals(template.escape("bar"), "foobar") + + def test_init__default_encoding__default(self): + """ + Check the default value. + + """ + template = Template() + self.assertEquals(template.default_encoding, sys.getdefaultencoding()) + + def test_init__default_encoding(self): + """ + Check that the constructor sets the attribute correctly. + + """ + template = Template(default_encoding="foo") + self.assertEquals(template.default_encoding, "foo") + + def test_init__decode_errors__default(self): + """ + Check the default value. + + """ + template = Template() + self.assertEquals(template.decode_errors, 'strict') + + def test_init__decode_errors(self): + """ + Check that the constructor sets the attribute correctly. + + """ + template = Template(decode_errors="foo") + self.assertEquals(template.decode_errors, "foo") + + def test_unicode(self): + template = Template() + actual = template.literal("abc") + self.assertEquals(actual, "abc") + self.assertEquals(type(actual), unicode) + + def test_unicode__default_encoding(self): + template = Template() + s = "é" + + template.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, template.unicode, s) + + template.default_encoding = "utf-8" + self.assertEquals(template.unicode(s), u"é") + + def test_unicode__decode_errors(self): + template = Template() + s = "é" + + template.default_encoding = "ascii" + template.decode_errors = "strict" + self.assertRaises(UnicodeDecodeError, template.unicode, s) + + template.decode_errors = "replace" + # U+FFFD is the official Unicode replacement character. + self.assertEquals(template.unicode(s), u'\ufffd\ufffd') + + def test_literal__with_markupsafe(self): + if not self._was_markupsafe_imported(): + # Then we cannot test this case. + return + self._restore_markupsafe() + + _template = Template() + _template.default_encoding = "utf_8" + + # Check the standard case. + actual = _template.literal("abc") + self.assertEquals(actual, "abc") + self.assertEquals(type(actual), renderer.markupsafe.Markup) + + s = "é" + # Check that markupsafe respects default_encoding. + self.assertEquals(_template.literal(s), u"é") + _template.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, _template.literal, s) + + # Check that markupsafe respects decode_errors. + _template.decode_errors = "replace" + self.assertEquals(_template.literal(s), u'\ufffd\ufffd') + + def test_render__unicode(self): + template = Template(u'foo') + actual = template.render() + self.assertTrue(isinstance(actual, unicode)) + self.assertEquals(actual, u'foo') + + def test_render__str(self): + template = Template('foo') + actual = template.render() + self.assertTrue(isinstance(actual, unicode)) + self.assertEquals(actual, 'foo') + + def test_render__non_ascii_character(self): + template = Template(u'Poincaré') + actual = template.render() + self.assertTrue(isinstance(actual, unicode)) + self.assertEquals(actual, u'Poincaré') + + def test_render__context(self): + """ + Test render(): passing a context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render({'person': 'Mom'}), 'Hi Mom') + + def test_render__context_and_kwargs(self): + """ + Test render(): passing a context and **kwargs. + + """ + template = Template('Hi {{person1}} and {{person2}}') + self.assertEquals(template.render({'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') + + def test_render__kwargs_and_no_context(self): + """ + Test render(): passing **kwargs and no context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render(person='Mom'), 'Hi Mom') + + def test_render__context_and_kwargs__precedence(self): + """ + Test render(): **kwargs takes precedence over context. + + """ + template = Template('Hi {{person}}') + self.assertEquals(template.render({'person': 'Mom'}, person='Dad'), 'Hi Dad') + + def test_render__kwargs_does_not_modify_context(self): + """ + Test render(): passing **kwargs does not modify the passed context. + + """ + context = {} + template = Template('Hi {{person}}') + template.render(context=context, foo="bar") + self.assertEquals(context, {}) + + def test_render__output_encoding(self): + template = Template(u'Poincaré') + template.output_encoding = 'utf-8' + actual = template.render() + self.assertTrue(isinstance(actual, str)) + self.assertEquals(actual, 'Poincaré') + + def test_render__nonascii_template(self): + """ + Test passing a non-unicode template with non-ascii characters. + + """ + template = Template("déf", output_encoding="utf-8") + + # Check that decode_errors and default_encoding are both respected. + template.decode_errors = 'ignore' + template.default_encoding = 'ascii' + self.assertEquals(template.render(), "df") + + template.default_encoding = 'utf_8' + self.assertEquals(template.render(), "déf") + + # By testing that Template.render() constructs the RenderEngine instance + # correctly, we no longer need to test the rendering code paths through + # the Template. We can test rendering paths through only the RenderEngine + # for the same amount of code coverage. + def test_make_render_engine__load_template(self): + """ + Test that _make_render_engine() passes the right load_template. + + """ + template = Template() + template.load_template = "foo" # in real life, this would be a function. + + engine = template._make_render_engine() + self.assertEquals(engine.load_template, "foo") + + def test_make_render_engine__literal(self): + """ + Test that _make_render_engine() passes the right literal. + + """ + template = Template() + template.literal = "foo" # in real life, this would be a function. + + engine = template._make_render_engine() + self.assertEquals(engine.literal, "foo") + + def test_make_render_engine__escape(self): + """ + Test that _make_render_engine() passes the right escape. + + """ + template = Template() + template.unicode = lambda s: s.upper() # a test version. + template.escape = lambda s: "**" + s # a test version. + + engine = template._make_render_engine() + escape = engine.escape + + self.assertEquals(escape(u"foo"), "**foo") + + # Test that escape converts str strings to unicode first. + self.assertEquals(escape("foo"), "**FOO") diff --git a/tests/test_template.py b/tests/test_template.py deleted file mode 100644 index 0944703..0000000 --- a/tests/test_template.py +++ /dev/null @@ -1,279 +0,0 @@ -# coding: utf-8 - -""" -Unit tests of template.py. - -""" - -import codecs -import sys -import unittest - -from pystache import template -from pystache.template import Template - - -class TemplateTestCase(unittest.TestCase): - - """Test the Template class.""" - - def setUp(self): - """ - Disable markupsafe. - - """ - self.original_markupsafe = template.markupsafe - template.markupsafe = None - - def tearDown(self): - self._restore_markupsafe() - - def _was_markupsafe_imported(self): - return bool(self.original_markupsafe) - - def _restore_markupsafe(self): - """ - Restore markupsafe to its original state. - - """ - template.markupsafe = self.original_markupsafe - - def test__was_markupsafe_imported(self): - """ - Test that our helper function works. - - """ - markupsafe = None - try: - import markupsafe - except: - pass - - self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) - - def test_init__escape__default_without_markupsafe(self): - template = Template() - self.assertEquals(template.escape(">'"), ">'") - - def test_init__escape__default_with_markupsafe(self): - if not self._was_markupsafe_imported(): - # Then we cannot test this case. - return - self._restore_markupsafe() - - template = Template() - self.assertEquals(template.escape(">'"), ">'") - - def test_init__escape(self): - escape = lambda s: "foo" + s - template = Template(escape=escape) - self.assertEquals(template.escape("bar"), "foobar") - - def test_init__default_encoding__default(self): - """ - Check the default value. - - """ - template = Template() - self.assertEquals(template.default_encoding, sys.getdefaultencoding()) - - def test_init__default_encoding(self): - """ - Check that the constructor sets the attribute correctly. - - """ - template = Template(default_encoding="foo") - self.assertEquals(template.default_encoding, "foo") - - def test_init__decode_errors__default(self): - """ - Check the default value. - - """ - template = Template() - self.assertEquals(template.decode_errors, 'strict') - - def test_init__decode_errors(self): - """ - Check that the constructor sets the attribute correctly. - - """ - template = Template(decode_errors="foo") - self.assertEquals(template.decode_errors, "foo") - - def test_unicode(self): - template = Template() - actual = template.literal("abc") - self.assertEquals(actual, "abc") - self.assertEquals(type(actual), unicode) - - def test_unicode__default_encoding(self): - template = Template() - s = "é" - - template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, template.unicode, s) - - template.default_encoding = "utf-8" - self.assertEquals(template.unicode(s), u"é") - - def test_unicode__decode_errors(self): - template = Template() - s = "é" - - template.default_encoding = "ascii" - template.decode_errors = "strict" - self.assertRaises(UnicodeDecodeError, template.unicode, s) - - template.decode_errors = "replace" - # U+FFFD is the official Unicode replacement character. - self.assertEquals(template.unicode(s), u'\ufffd\ufffd') - - def test_literal__with_markupsafe(self): - if not self._was_markupsafe_imported(): - # Then we cannot test this case. - return - self._restore_markupsafe() - - _template = Template() - _template.default_encoding = "utf_8" - - # Check the standard case. - actual = _template.literal("abc") - self.assertEquals(actual, "abc") - self.assertEquals(type(actual), template.markupsafe.Markup) - - s = "é" - # Check that markupsafe respects default_encoding. - self.assertEquals(_template.literal(s), u"é") - _template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, _template.literal, s) - - # Check that markupsafe respects decode_errors. - _template.decode_errors = "replace" - self.assertEquals(_template.literal(s), u'\ufffd\ufffd') - - def test_render__unicode(self): - template = Template(u'foo') - actual = template.render() - self.assertTrue(isinstance(actual, unicode)) - self.assertEquals(actual, u'foo') - - def test_render__str(self): - template = Template('foo') - actual = template.render() - self.assertTrue(isinstance(actual, unicode)) - self.assertEquals(actual, 'foo') - - def test_render__non_ascii_character(self): - template = Template(u'Poincaré') - actual = template.render() - self.assertTrue(isinstance(actual, unicode)) - self.assertEquals(actual, u'Poincaré') - - def test_render__context(self): - """ - Test render(): passing a context. - - """ - template = Template('Hi {{person}}') - self.assertEquals(template.render({'person': 'Mom'}), 'Hi Mom') - - def test_render__context_and_kwargs(self): - """ - Test render(): passing a context and **kwargs. - - """ - template = Template('Hi {{person1}} and {{person2}}') - self.assertEquals(template.render({'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') - - def test_render__kwargs_and_no_context(self): - """ - Test render(): passing **kwargs and no context. - - """ - template = Template('Hi {{person}}') - self.assertEquals(template.render(person='Mom'), 'Hi Mom') - - def test_render__context_and_kwargs__precedence(self): - """ - Test render(): **kwargs takes precedence over context. - - """ - template = Template('Hi {{person}}') - self.assertEquals(template.render({'person': 'Mom'}, person='Dad'), 'Hi Dad') - - def test_render__kwargs_does_not_modify_context(self): - """ - Test render(): passing **kwargs does not modify the passed context. - - """ - context = {} - template = Template('Hi {{person}}') - template.render(context=context, foo="bar") - self.assertEquals(context, {}) - - def test_render__output_encoding(self): - template = Template(u'Poincaré') - template.output_encoding = 'utf-8' - actual = template.render() - self.assertTrue(isinstance(actual, str)) - self.assertEquals(actual, 'Poincaré') - - def test_render__nonascii_template(self): - """ - Test passing a non-unicode template with non-ascii characters. - - """ - template = Template("déf", output_encoding="utf-8") - - # Check that decode_errors and default_encoding are both respected. - template.decode_errors = 'ignore' - template.default_encoding = 'ascii' - self.assertEquals(template.render(), "df") - - template.default_encoding = 'utf_8' - self.assertEquals(template.render(), "déf") - - # By testing that Template.render() constructs the RenderEngine instance - # correctly, we no longer need to test the rendering code paths through - # the Template. We can test rendering paths through only the RenderEngine - # for the same amount of code coverage. - def test_make_render_engine__load_template(self): - """ - Test that _make_render_engine() passes the right load_template. - - """ - template = Template() - template.load_template = "foo" # in real life, this would be a function. - - engine = template._make_render_engine() - self.assertEquals(engine.load_template, "foo") - - def test_make_render_engine__literal(self): - """ - Test that _make_render_engine() passes the right literal. - - """ - template = Template() - template.literal = "foo" # in real life, this would be a function. - - engine = template._make_render_engine() - self.assertEquals(engine.literal, "foo") - - def test_make_render_engine__escape(self): - """ - Test that _make_render_engine() passes the right escape. - - """ - template = Template() - template.unicode = lambda s: s.upper() # a test version. - template.escape = lambda s: "**" + s # a test version. - - engine = template._make_render_engine() - escape = engine.escape - - self.assertEquals(escape(u"foo"), "**foo") - - # Test that escape converts str strings to unicode first. - self.assertEquals(escape("foo"), "**FOO") -- cgit v1.2.1 From d4125da548c02e5cbe7b6ec3bc863c71db94b735 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:10:16 -0800 Subject: Renamed Template class to Renderer. --- pystache/commands.py | 4 +-- pystache/init.py | 6 ++--- pystache/renderer.py | 2 +- pystache/view.py | 6 ++--- tests/test_renderer.py | 72 +++++++++++++++++++++++++------------------------- tests/test_simple.py | 6 ++--- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 5326281..131e1f1 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -20,7 +20,7 @@ import sys # ValueError: Attempted relative import in non-package # from pystache.loader import Loader -from pystache.renderer import Template +from pystache.renderer import Renderer USAGE = """\ @@ -64,7 +64,7 @@ def main(sys_argv): except IOError: context = json.loads(context) - template = Template(template) + template = Renderer(template) print(template.render(context)) diff --git a/pystache/init.py b/pystache/init.py index 48c8a2d..5d0859a 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,12 +5,12 @@ This module contains the initialization logic called by __init__.py. """ -from .renderer import Template +from .renderer import Renderer from .view import View from .loader import Loader -__all__ = ['Template', 'View', 'Loader', 'render'] +__all__ = ['render', 'Loader', 'Renderer', 'View'] def render(template, context=None, **kwargs): @@ -18,5 +18,5 @@ def render(template, context=None, **kwargs): Return the given template string rendered using the given context. """ - template = Template(template) + template = Renderer(template) return template.render(context, **kwargs) diff --git a/pystache/renderer.py b/pystache/renderer.py index 1754243..c611a4a 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -20,7 +20,7 @@ except ImportError: pass -class Template(object): +class Renderer(object): def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, default_encoding=None, decode_errors='strict'): diff --git a/pystache/view.py b/pystache/view.py index 452cb30..de1e4af 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -10,7 +10,7 @@ from types import UnboundMethodType from .context import Context from .loader import Loader -from .renderer import Template +from .renderer import Renderer class View(object): @@ -68,7 +68,7 @@ class View(object): def _get_template_name(self): """ - Return the name of this Template instance. + Return the name of the template to load. If the template_name attribute is not set, then this method constructs the template name from the class name as follows, for example: @@ -98,7 +98,7 @@ class View(object): Return the view rendered using the current context. """ - template = Template(self.get_template(), self.load_template, output_encoding=encoding, + template = Renderer(self.get_template(), self.load_template, output_encoding=encoding, escape=escape) return template.render(self.context) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index e17ea63..efd0098 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -10,12 +10,12 @@ import sys import unittest from pystache import renderer -from pystache.renderer import Template +from pystache.renderer import Renderer -class TemplateTestCase(unittest.TestCase): +class RendererTestCase(unittest.TestCase): - """Test the Template class.""" + """Test the Renderer class.""" def setUp(self): """ @@ -52,7 +52,7 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) def test_init__escape__default_without_markupsafe(self): - template = Template() + template = Renderer() self.assertEquals(template.escape(">'"), ">'") def test_init__escape__default_with_markupsafe(self): @@ -61,12 +61,12 @@ class TemplateTestCase(unittest.TestCase): return self._restore_markupsafe() - template = Template() + template = Renderer() self.assertEquals(template.escape(">'"), ">'") def test_init__escape(self): escape = lambda s: "foo" + s - template = Template(escape=escape) + template = Renderer(escape=escape) self.assertEquals(template.escape("bar"), "foobar") def test_init__default_encoding__default(self): @@ -74,7 +74,7 @@ class TemplateTestCase(unittest.TestCase): Check the default value. """ - template = Template() + template = Renderer() self.assertEquals(template.default_encoding, sys.getdefaultencoding()) def test_init__default_encoding(self): @@ -82,7 +82,7 @@ class TemplateTestCase(unittest.TestCase): Check that the constructor sets the attribute correctly. """ - template = Template(default_encoding="foo") + template = Renderer(default_encoding="foo") self.assertEquals(template.default_encoding, "foo") def test_init__decode_errors__default(self): @@ -90,7 +90,7 @@ class TemplateTestCase(unittest.TestCase): Check the default value. """ - template = Template() + template = Renderer() self.assertEquals(template.decode_errors, 'strict') def test_init__decode_errors(self): @@ -98,17 +98,17 @@ class TemplateTestCase(unittest.TestCase): Check that the constructor sets the attribute correctly. """ - template = Template(decode_errors="foo") + template = Renderer(decode_errors="foo") self.assertEquals(template.decode_errors, "foo") def test_unicode(self): - template = Template() + template = Renderer() actual = template.literal("abc") self.assertEquals(actual, "abc") self.assertEquals(type(actual), unicode) def test_unicode__default_encoding(self): - template = Template() + template = Renderer() s = "é" template.default_encoding = "ascii" @@ -118,7 +118,7 @@ class TemplateTestCase(unittest.TestCase): self.assertEquals(template.unicode(s), u"é") def test_unicode__decode_errors(self): - template = Template() + template = Renderer() s = "é" template.default_encoding = "ascii" @@ -135,38 +135,38 @@ class TemplateTestCase(unittest.TestCase): return self._restore_markupsafe() - _template = Template() - _template.default_encoding = "utf_8" + template = Renderer() + template.default_encoding = "utf_8" # Check the standard case. - actual = _template.literal("abc") + actual = template.literal("abc") self.assertEquals(actual, "abc") self.assertEquals(type(actual), renderer.markupsafe.Markup) s = "é" # Check that markupsafe respects default_encoding. - self.assertEquals(_template.literal(s), u"é") - _template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, _template.literal, s) + self.assertEquals(template.literal(s), u"é") + template.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, template.literal, s) # Check that markupsafe respects decode_errors. - _template.decode_errors = "replace" - self.assertEquals(_template.literal(s), u'\ufffd\ufffd') + template.decode_errors = "replace" + self.assertEquals(template.literal(s), u'\ufffd\ufffd') def test_render__unicode(self): - template = Template(u'foo') + template = Renderer(u'foo') actual = template.render() self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'foo') def test_render__str(self): - template = Template('foo') + template = Renderer('foo') actual = template.render() self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, 'foo') def test_render__non_ascii_character(self): - template = Template(u'Poincaré') + template = Renderer(u'Poincaré') actual = template.render() self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'Poincaré') @@ -176,7 +176,7 @@ class TemplateTestCase(unittest.TestCase): Test render(): passing a context. """ - template = Template('Hi {{person}}') + template = Renderer('Hi {{person}}') self.assertEquals(template.render({'person': 'Mom'}), 'Hi Mom') def test_render__context_and_kwargs(self): @@ -184,7 +184,7 @@ class TemplateTestCase(unittest.TestCase): Test render(): passing a context and **kwargs. """ - template = Template('Hi {{person1}} and {{person2}}') + template = Renderer('Hi {{person1}} and {{person2}}') self.assertEquals(template.render({'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') def test_render__kwargs_and_no_context(self): @@ -192,7 +192,7 @@ class TemplateTestCase(unittest.TestCase): Test render(): passing **kwargs and no context. """ - template = Template('Hi {{person}}') + template = Renderer('Hi {{person}}') self.assertEquals(template.render(person='Mom'), 'Hi Mom') def test_render__context_and_kwargs__precedence(self): @@ -200,7 +200,7 @@ class TemplateTestCase(unittest.TestCase): Test render(): **kwargs takes precedence over context. """ - template = Template('Hi {{person}}') + template = Renderer('Hi {{person}}') self.assertEquals(template.render({'person': 'Mom'}, person='Dad'), 'Hi Dad') def test_render__kwargs_does_not_modify_context(self): @@ -209,12 +209,12 @@ class TemplateTestCase(unittest.TestCase): """ context = {} - template = Template('Hi {{person}}') + template = Renderer('Hi {{person}}') template.render(context=context, foo="bar") self.assertEquals(context, {}) def test_render__output_encoding(self): - template = Template(u'Poincaré') + template = Renderer(u'Poincaré') template.output_encoding = 'utf-8' actual = template.render() self.assertTrue(isinstance(actual, str)) @@ -225,7 +225,7 @@ class TemplateTestCase(unittest.TestCase): Test passing a non-unicode template with non-ascii characters. """ - template = Template("déf", output_encoding="utf-8") + template = Renderer("déf", output_encoding="utf-8") # Check that decode_errors and default_encoding are both respected. template.decode_errors = 'ignore' @@ -235,16 +235,16 @@ class TemplateTestCase(unittest.TestCase): template.default_encoding = 'utf_8' self.assertEquals(template.render(), "déf") - # By testing that Template.render() constructs the RenderEngine instance + # By testing that Renderer.render() constructs the RenderEngine instance # correctly, we no longer need to test the rendering code paths through - # the Template. We can test rendering paths through only the RenderEngine + # the Renderer. We can test rendering paths through only the RenderEngine # for the same amount of code coverage. def test_make_render_engine__load_template(self): """ Test that _make_render_engine() passes the right load_template. """ - template = Template() + template = Renderer() template.load_template = "foo" # in real life, this would be a function. engine = template._make_render_engine() @@ -255,7 +255,7 @@ class TemplateTestCase(unittest.TestCase): Test that _make_render_engine() passes the right literal. """ - template = Template() + template = Renderer() template.literal = "foo" # in real life, this would be a function. engine = template._make_render_engine() @@ -266,7 +266,7 @@ class TemplateTestCase(unittest.TestCase): Test that _make_render_engine() passes the right escape. """ - template = Template() + template = Renderer() template.unicode = lambda s: s.upper() # a test version. template.escape = lambda s: "**" + s # a test version. diff --git a/tests/test_simple.py b/tests/test_simple.py index 365c82d..897c7a2 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,6 +1,6 @@ import unittest import pystache -from pystache import Template +from pystache import Renderer from examples.nested_context import NestedContext from examples.complex_view import ComplexView from examples.lambdas import Lambdas @@ -21,7 +21,7 @@ class TestSimple(unittest.TestCase): def test_empty_context(self): view = ComplexView() - self.assertEquals(pystache.Template('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me") + self.assertEquals(pystache.Renderer('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me") def test_callables(self): view = Lambdas() @@ -39,7 +39,7 @@ class TestSimple(unittest.TestCase): def test_non_existent_value_renders_blank(self): view = Simple() - self.assertEquals(pystache.Template('{{not_set}} {{blank}}', view).render(), ' ') + self.assertEquals(pystache.Renderer('{{not_set}} {{blank}}', view).render(), ' ') def test_template_partial_extension(self): -- cgit v1.2.1 From 3022cfb9adc75d842d2f5e91667d562456b17601 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:12:19 -0800 Subject: Renamed template variables to renderer in non-test files. --- pystache/commands.py | 4 ++-- pystache/init.py | 4 ++-- pystache/view.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 131e1f1..830b793 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -64,8 +64,8 @@ def main(sys_argv): except IOError: context = json.loads(context) - template = Renderer(template) - print(template.render(context)) + renderer = Renderer(template) + print(renderer.render(context)) if __name__=='__main__': diff --git a/pystache/init.py b/pystache/init.py index 5d0859a..f087752 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -18,5 +18,5 @@ def render(template, context=None, **kwargs): Return the given template string rendered using the given context. """ - template = Renderer(template) - return template.render(context, **kwargs) + renderer = Renderer(template) + return renderer.render(context, **kwargs) diff --git a/pystache/view.py b/pystache/view.py index de1e4af..2cdc8b5 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -98,9 +98,9 @@ class View(object): Return the view rendered using the current context. """ - template = Renderer(self.get_template(), self.load_template, output_encoding=encoding, + renderer = Renderer(self.get_template(), self.load_template, output_encoding=encoding, escape=escape) - return template.render(self.context) + return renderer.render(self.context) def get(self, key, default=None): return self.context.get(key, default) -- cgit v1.2.1 From 315edcd67997efbc109d9f47f5ee09173eb54815 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:14:14 -0800 Subject: Renamed template to renderer in test files. --- tests/test_renderer.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index efd0098..7d2bf62 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -244,10 +244,10 @@ class RendererTestCase(unittest.TestCase): Test that _make_render_engine() passes the right load_template. """ - template = Renderer() - template.load_template = "foo" # in real life, this would be a function. + renderer = Renderer() + renderer.load_template = "foo" # in real life, this would be a function. - engine = template._make_render_engine() + engine = renderer._make_render_engine() self.assertEquals(engine.load_template, "foo") def test_make_render_engine__literal(self): @@ -255,10 +255,10 @@ class RendererTestCase(unittest.TestCase): Test that _make_render_engine() passes the right literal. """ - template = Renderer() - template.literal = "foo" # in real life, this would be a function. + renderer = Renderer() + renderer.literal = "foo" # in real life, this would be a function. - engine = template._make_render_engine() + engine = renderer._make_render_engine() self.assertEquals(engine.literal, "foo") def test_make_render_engine__escape(self): @@ -266,11 +266,11 @@ class RendererTestCase(unittest.TestCase): Test that _make_render_engine() passes the right escape. """ - template = Renderer() - template.unicode = lambda s: s.upper() # a test version. - template.escape = lambda s: "**" + s # a test version. + renderer = Renderer() + renderer.unicode = lambda s: s.upper() # a test version. + renderer.escape = lambda s: "**" + s # a test version. - engine = template._make_render_engine() + engine = renderer._make_render_engine() escape = engine.escape self.assertEquals(escape(u"foo"), "**foo") -- cgit v1.2.1 From 9aaf2e1d54d67c5cc28531864699213f3b0b131e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:31:16 -0800 Subject: Moved Template() template argument to render() argument. --- pystache/commands.py | 6 ++- pystache/init.py | 4 +- pystache/renderer.py | 25 +++++----- pystache/view.py | 6 +-- tests/test_renderer.py | 125 +++++++++++++++++++++++++------------------------ tests/test_simple.py | 7 +-- 6 files changed, 90 insertions(+), 83 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 830b793..f2adf6e 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -64,8 +64,10 @@ def main(sys_argv): except IOError: context = json.loads(context) - renderer = Renderer(template) - print(renderer.render(context)) + renderer = Renderer() + + rendered = renderer.render(template, context) + print rendered if __name__=='__main__': diff --git a/pystache/init.py b/pystache/init.py index f087752..7d5d4d7 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -18,5 +18,5 @@ def render(template, context=None, **kwargs): Return the given template string rendered using the given context. """ - renderer = Renderer(template) - return renderer.render(context, **kwargs) + renderer = Renderer() + return renderer.render(template, context, **kwargs) diff --git a/pystache/renderer.py b/pystache/renderer.py index c611a4a..2a0b188 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -22,17 +22,14 @@ except ImportError: class Renderer(object): - def __init__(self, template=None, load_template=None, output_encoding=None, escape=None, + # TODO: change load_template to load_partial. + def __init__(self, load_template=None, output_encoding=None, escape=None, default_encoding=None, decode_errors='strict'): """ - Construct a Template instance. + Construct an instance. Arguments: - template: a template string that is either unicode, or of type - str and encoded using the encoding named by the default_encoding - keyword argument. - load_template: the function for loading partials. The function should accept a single template_name parameter and return a template as a string. Defaults to the default Loader's load_template() method. @@ -84,7 +81,6 @@ class Renderer(object): self.escape = escape self.load_template = load_template self.output_encoding = output_encoding - self.template = template def _unicode_and_escape(self, s): if not isinstance(s, unicode): @@ -146,9 +142,9 @@ class Renderer(object): escape=self._unicode_and_escape) return engine - def render(self, context=None, **kwargs): + def render(self, template, context=None, **kwargs): """ - Return the template rendered using the given context. + Render the given template using the given context. The return value is a unicode string, unless the output_encoding attribute has been set to a non-None value, in which case the @@ -160,6 +156,10 @@ class Renderer(object): Arguments: + template: a template string that is either unicode, or of type + str and encoded using the encoding named by the default_encoding + keyword argument. + context: a dictionary, Context, or object (e.g. a View instance). **kwargs: additional key values to add to the context when rendering. @@ -169,13 +169,12 @@ class Renderer(object): engine = self._make_render_engine() context = self._make_context(context, **kwargs) - template = self.template if not isinstance(template, unicode): template = self.unicode(template) - result = engine.render(template, context) + rendered = engine.render(template, context) if self.output_encoding is not None: - result = result.encode(self.output_encoding) + rendered = rendered.encode(self.output_encoding) - return result + return rendered diff --git a/pystache/view.py b/pystache/view.py index 2cdc8b5..f0dd335 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -98,9 +98,9 @@ class View(object): Return the view rendered using the current context. """ - renderer = Renderer(self.get_template(), self.load_template, output_encoding=encoding, - escape=escape) - return renderer.render(self.context) + template = self.get_template() + renderer = Renderer(self.load_template, output_encoding=encoding, escape=escape) + return renderer.render(template, self.context) def get(self, key, default=None): return self.context.get(key, default) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 7d2bf62..051bb11 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -28,6 +28,9 @@ class RendererTestCase(unittest.TestCase): def tearDown(self): self._restore_markupsafe() + def _renderer(self): + return Renderer() + def _was_markupsafe_imported(self): return bool(self.original_markupsafe) @@ -52,8 +55,8 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) def test_init__escape__default_without_markupsafe(self): - template = Renderer() - self.assertEquals(template.escape(">'"), ">'") + renderer = Renderer() + self.assertEquals(renderer.escape(">'"), ">'") def test_init__escape__default_with_markupsafe(self): if not self._was_markupsafe_imported(): @@ -61,73 +64,73 @@ class RendererTestCase(unittest.TestCase): return self._restore_markupsafe() - template = Renderer() - self.assertEquals(template.escape(">'"), ">'") + renderer = Renderer() + self.assertEquals(renderer.escape(">'"), ">'") def test_init__escape(self): escape = lambda s: "foo" + s - template = Renderer(escape=escape) - self.assertEquals(template.escape("bar"), "foobar") + renderer = Renderer(escape=escape) + self.assertEquals(renderer.escape("bar"), "foobar") def test_init__default_encoding__default(self): """ Check the default value. """ - template = Renderer() - self.assertEquals(template.default_encoding, sys.getdefaultencoding()) + renderer = Renderer() + self.assertEquals(renderer.default_encoding, sys.getdefaultencoding()) def test_init__default_encoding(self): """ Check that the constructor sets the attribute correctly. """ - template = Renderer(default_encoding="foo") - self.assertEquals(template.default_encoding, "foo") + renderer = Renderer(default_encoding="foo") + self.assertEquals(renderer.default_encoding, "foo") def test_init__decode_errors__default(self): """ Check the default value. """ - template = Renderer() - self.assertEquals(template.decode_errors, 'strict') + renderer = Renderer() + self.assertEquals(renderer.decode_errors, 'strict') def test_init__decode_errors(self): """ Check that the constructor sets the attribute correctly. """ - template = Renderer(decode_errors="foo") - self.assertEquals(template.decode_errors, "foo") + renderer = Renderer(decode_errors="foo") + self.assertEquals(renderer.decode_errors, "foo") def test_unicode(self): - template = Renderer() - actual = template.literal("abc") + renderer = Renderer() + actual = renderer.literal("abc") self.assertEquals(actual, "abc") self.assertEquals(type(actual), unicode) def test_unicode__default_encoding(self): - template = Renderer() + renderer = Renderer() s = "é" - template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, template.unicode, s) + renderer.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, renderer.unicode, s) - template.default_encoding = "utf-8" - self.assertEquals(template.unicode(s), u"é") + renderer.default_encoding = "utf-8" + self.assertEquals(renderer.unicode(s), u"é") def test_unicode__decode_errors(self): - template = Renderer() + renderer = Renderer() s = "é" - template.default_encoding = "ascii" - template.decode_errors = "strict" - self.assertRaises(UnicodeDecodeError, template.unicode, s) + renderer.default_encoding = "ascii" + renderer.decode_errors = "strict" + self.assertRaises(UnicodeDecodeError, renderer.unicode, s) - template.decode_errors = "replace" + renderer.decode_errors = "replace" # U+FFFD is the official Unicode replacement character. - self.assertEquals(template.unicode(s), u'\ufffd\ufffd') + self.assertEquals(renderer.unicode(s), u'\ufffd\ufffd') def test_literal__with_markupsafe(self): if not self._was_markupsafe_imported(): @@ -135,39 +138,39 @@ class RendererTestCase(unittest.TestCase): return self._restore_markupsafe() - template = Renderer() - template.default_encoding = "utf_8" + _renderer = Renderer() + _renderer.default_encoding = "utf_8" # Check the standard case. - actual = template.literal("abc") + actual = _renderer.literal("abc") self.assertEquals(actual, "abc") self.assertEquals(type(actual), renderer.markupsafe.Markup) s = "é" # Check that markupsafe respects default_encoding. - self.assertEquals(template.literal(s), u"é") - template.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, template.literal, s) + self.assertEquals(_renderer.literal(s), u"é") + _renderer.default_encoding = "ascii" + self.assertRaises(UnicodeDecodeError, _renderer.literal, s) # Check that markupsafe respects decode_errors. - template.decode_errors = "replace" - self.assertEquals(template.literal(s), u'\ufffd\ufffd') + _renderer.decode_errors = "replace" + self.assertEquals(_renderer.literal(s), u'\ufffd\ufffd') def test_render__unicode(self): - template = Renderer(u'foo') - actual = template.render() + renderer = Renderer() + actual = renderer.render(u'foo') self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'foo') def test_render__str(self): - template = Renderer('foo') - actual = template.render() + renderer = Renderer() + actual = renderer.render('foo') self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, 'foo') def test_render__non_ascii_character(self): - template = Renderer(u'Poincaré') - actual = template.render() + renderer = Renderer() + actual = renderer.render(u'Poincaré') self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'Poincaré') @@ -176,32 +179,33 @@ class RendererTestCase(unittest.TestCase): Test render(): passing a context. """ - template = Renderer('Hi {{person}}') - self.assertEquals(template.render({'person': 'Mom'}), 'Hi Mom') + renderer = Renderer() + self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom') def test_render__context_and_kwargs(self): """ Test render(): passing a context and **kwargs. """ - template = Renderer('Hi {{person1}} and {{person2}}') - self.assertEquals(template.render({'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') + renderer = Renderer() + template = 'Hi {{person1}} and {{person2}}' + self.assertEquals(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') def test_render__kwargs_and_no_context(self): """ Test render(): passing **kwargs and no context. """ - template = Renderer('Hi {{person}}') - self.assertEquals(template.render(person='Mom'), 'Hi Mom') + renderer = Renderer() + self.assertEquals(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom') def test_render__context_and_kwargs__precedence(self): """ Test render(): **kwargs takes precedence over context. """ - template = Renderer('Hi {{person}}') - self.assertEquals(template.render({'person': 'Mom'}, person='Dad'), 'Hi Dad') + renderer = Renderer() + self.assertEquals(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad') def test_render__kwargs_does_not_modify_context(self): """ @@ -209,14 +213,14 @@ class RendererTestCase(unittest.TestCase): """ context = {} - template = Renderer('Hi {{person}}') - template.render(context=context, foo="bar") + renderer = Renderer() + renderer.render('Hi {{person}}', context=context, foo="bar") self.assertEquals(context, {}) def test_render__output_encoding(self): - template = Renderer(u'Poincaré') - template.output_encoding = 'utf-8' - actual = template.render() + renderer = Renderer() + renderer.output_encoding = 'utf-8' + actual = renderer.render(u'Poincaré') self.assertTrue(isinstance(actual, str)) self.assertEquals(actual, 'Poincaré') @@ -225,15 +229,16 @@ class RendererTestCase(unittest.TestCase): Test passing a non-unicode template with non-ascii characters. """ - template = Renderer("déf", output_encoding="utf-8") + renderer = Renderer(output_encoding="utf-8") + template = "déf" # Check that decode_errors and default_encoding are both respected. - template.decode_errors = 'ignore' - template.default_encoding = 'ascii' - self.assertEquals(template.render(), "df") + renderer.decode_errors = 'ignore' + renderer.default_encoding = 'ascii' + self.assertEquals(renderer.render(template), "df") - template.default_encoding = 'utf_8' - self.assertEquals(template.render(), "déf") + renderer.default_encoding = 'utf_8' + self.assertEquals(renderer.render(template), "déf") # By testing that Renderer.render() constructs the RenderEngine instance # correctly, we no longer need to test the rendering code paths through diff --git a/tests/test_simple.py b/tests/test_simple.py index 897c7a2..cd3dec8 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -21,7 +21,8 @@ class TestSimple(unittest.TestCase): def test_empty_context(self): view = ComplexView() - self.assertEquals(pystache.Renderer('{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}', view).render(), "Should see me") + template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' + self.assertEquals(pystache.Renderer().render(template), "Should see me") def test_callables(self): view = Lambdas() @@ -38,8 +39,8 @@ class TestSimple(unittest.TestCase): def test_non_existent_value_renders_blank(self): view = Simple() - - self.assertEquals(pystache.Renderer('{{not_set}} {{blank}}', view).render(), ' ') + template = '{{not_set}} {{blank}}' + self.assertEquals(pystache.Renderer().render(template), ' ') def test_template_partial_extension(self): -- cgit v1.2.1 From a36cd00a71efa1cd667152b973e8dfc3efaf5fc5 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:33:35 -0800 Subject: Tweaked a docstring. --- pystache/renderer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 2a0b188..abd9a62 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -30,9 +30,10 @@ class Renderer(object): Arguments: - load_template: the function for loading partials. The function should - accept a single template_name parameter and return a template as - a string. Defaults to the default Loader's load_template() method. + load_template: a function for loading templates by name, for + example when loading partials. The function should accept a + single template_name parameter and return a template as a string. + Defaults to the default Loader's load_template() method. output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for -- cgit v1.2.1 From d494487a5966b0cc557f9f299db1826ff94f2812 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 19:36:29 -0800 Subject: Tweaked another docstring. --- pystache/renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index abd9a62..b3e5c02 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module provides a Template class. +This module provides a Renderer class to render templates. """ -- cgit v1.2.1 From d5cee636f2c4cfc7bb16b49327728425dedc8be9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 21:46:43 -0800 Subject: Fixes issue #61: "Renderer class should handle load_template not returning unicode" --- pystache/renderengine.py | 12 ++++++++---- pystache/renderer.py | 7 ++++++- tests/test_renderengine.py | 13 ++++++------- tests/test_renderer.py | 11 +++++++---- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index c94260f..b0b797c 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -55,10 +55,14 @@ class RenderEngine(object): modifiers = Modifiers() - def __init__(self, load_template=None, literal=None, escape=None): + def __init__(self, load_partial=None, literal=None, escape=None): """ Arguments: + load_partial: a function for loading templates by name when + loading partials. The function should accept a template name + and return a unicode template string. + escape: a function that takes a unicode or str string, converts it to unicode, and escapes and returns it. @@ -68,7 +72,7 @@ class RenderEngine(object): """ self.escape = escape self.literal = literal - self.load_template = load_template + self.load_partial = load_partial def render(self, template, context): """ @@ -178,7 +182,7 @@ class RenderEngine(object): @modifiers.set('>') def _render_partial(self, template_name): - markup = self.load_template(template_name) + markup = self.load_partial(template_name) return self._render(markup) @modifiers.set('=') @@ -194,7 +198,7 @@ class RenderEngine(object): @modifiers.set('{') @modifiers.set('&') - def render_unescaped(self, tag_name): + def _render_unescaped(self, tag_name): """ Render a tag without escaping it. diff --git a/pystache/renderer.py b/pystache/renderer.py index b3e5c02..4ba8b97 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -138,7 +138,12 @@ class Renderer(object): Return a RenderEngine instance for rendering. """ - engine = RenderEngine(load_template=self.load_template, + # Make sure the return value of load_template is unicode. + def load_partial(name): + template = self.load_template(name) + return self.unicode(template) + + engine = RenderEngine(load_partial=load_partial, literal=self.literal, escape=self._unicode_and_escape) return engine diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 6f75872..2962cbe 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -21,13 +21,12 @@ class RenderEngineTestCase(unittest.TestCase): Create and return a default RenderEngine for testing. """ - load_template = None to_unicode = unicode escape = lambda s: cgi.escape(to_unicode(s)) literal = to_unicode - engine = RenderEngine(literal=literal, escape=escape, load_template=None) + engine = RenderEngine(literal=literal, escape=escape, load_partial=None) return engine def _assert_render(self, expected, template, *context, **kwargs): @@ -35,7 +34,7 @@ class RenderEngineTestCase(unittest.TestCase): engine = kwargs.get('engine', self._engine()) if partials is not None: - engine.load_template = lambda key: partials[key] + engine.load_partial = lambda key: partials[key] context = Context(*context) @@ -49,23 +48,23 @@ class RenderEngineTestCase(unittest.TestCase): """ # In real-life, these arguments would be functions - engine = RenderEngine(load_template="load_template", literal="literal", escape="escape") + engine = RenderEngine(load_partial="foo", literal="literal", escape="escape") + self.assertEquals(engine.load_partial, "foo") self.assertEquals(engine.escape, "escape") self.assertEquals(engine.literal, "literal") - self.assertEquals(engine.load_template, "load_template") def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) - def test_render__load_template(self): + def test_render__load_partial(self): """ Test that render() uses the load_template attribute. """ engine = self._engine() partials = {'partial': "{{person}}"} - engine.load_template = lambda key: partials[key] + engine.load_partial = lambda key: partials[key] self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) def test_render__literal(self): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 051bb11..fd91f3c 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -244,16 +244,19 @@ class RendererTestCase(unittest.TestCase): # correctly, we no longer need to test the rendering code paths through # the Renderer. We can test rendering paths through only the RenderEngine # for the same amount of code coverage. - def test_make_render_engine__load_template(self): + def test_make_render_engine__load_partial(self): """ - Test that _make_render_engine() passes the right load_template. + Test that _make_render_engine() constructs and passes load_partial correctly. """ renderer = Renderer() - renderer.load_template = "foo" # in real life, this would be a function. + renderer.unicode = lambda s: s.upper() # a test version. + # In real-life, the partial would be different with each name. + renderer.load_template = lambda name: "partial" engine = renderer._make_render_engine() - self.assertEquals(engine.load_template, "foo") + # Make sure it calls unicode. + self.assertEquals(engine.load_partial('name'), "PARTIAL") def test_make_render_engine__literal(self): """ -- cgit v1.2.1 From b41b8e105ce6e63c6a97bb9ca0d8d929613d38f4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Dec 2011 22:25:29 -0800 Subject: Fixed issue #62: "Loader.load_template() should always return unicode" --- pystache/loader.py | 13 ++++++++++--- pystache/renderer.py | 24 ++++++++++++++++++++---- tests/test_loader.py | 30 ++++++++++++++++++++++-------- tests/test_renderer.py | 14 ++++++++++++++ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 83690d2..d92f168 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -6,7 +6,7 @@ This module provides a Loader class. """ import os - +import sys DEFAULT_EXTENSION = 'mustache' @@ -19,6 +19,11 @@ class Loader(object): Arguments: + encoding: the name of the encoding to use when converting file + contents to unicode. This name will be passed as the encoding + argument to the built-in function unicode(). Defaults to the + encoding name returned by sys.getdefaultencoding(). + search_dirs: the directories in which to search for templates. Defaults to the current working directory. @@ -26,6 +31,8 @@ class Loader(object): Pass False for no extension. """ + if encoding is None: + encoding = sys.getdefaultencoding() if extension is None: extension = DEFAULT_EXTENSION if search_dirs is None: @@ -73,9 +80,9 @@ class Loader(object): try: template = f.read() - if self.template_encoding: - template = unicode(template, self.template_encoding) finally: f.close() + template = unicode(template, self.template_encoding) + return template diff --git a/pystache/renderer.py b/pystache/renderer.py index 4ba8b97..36cd4b5 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -133,15 +133,31 @@ class Renderer(object): return context - def _make_render_engine(self): + def _make_load_partial(self): """ - Return a RenderEngine instance for rendering. + Return the load_partial function for use by RenderEngine. """ - # Make sure the return value of load_template is unicode. def load_partial(name): template = self.load_template(name) - return self.unicode(template) + # Make sure the return value of load_template is unicode since + # RenderEngine requires it. Also, check that the string is not + # already unicode to avoid "double-decoding". Otherwise, we + # would get the following error: + # TypeError: decoding Unicode is not supported + if not isinstance(template, unicode): + template = self.unicode(template) + + return template + + return load_partial + + def _make_render_engine(self): + """ + Return a RenderEngine instance for rendering. + + """ + load_partial = self._make_load_partial() engine = RenderEngine(load_partial=load_partial, literal=self.literal, diff --git a/tests/test_loader.py b/tests/test_loader.py index 70aacd9..92c6471 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,4 +1,5 @@ import os +import sys import unittest from pystache.loader import Loader @@ -8,18 +9,13 @@ class LoaderTestCase(unittest.TestCase): search_dirs = 'examples' - def test_init(self): - """ - Test the __init__() constructor. - - """ + def test_init__search_dirs(self): + # Test the default value. loader = Loader() self.assertEquals(loader.search_dirs, [os.curdir]) - self.assertTrue(loader.template_encoding is None) - loader = Loader(search_dirs=['foo'], encoding='utf-8') + loader = Loader(search_dirs=['foo']) self.assertEquals(loader.search_dirs, ['foo']) - self.assertEquals(loader.template_encoding, 'utf-8') def test_init__extension(self): # Test the default value. @@ -32,6 +28,14 @@ class LoaderTestCase(unittest.TestCase): loader = Loader(extension=False) self.assertTrue(loader.template_extension is False) + def test_init__loader(self): + # Test the default value. + loader = Loader() + self.assertEquals(loader.template_encoding, sys.getdefaultencoding()) + + loader = Loader(encoding='foo') + self.assertEquals(loader.template_encoding, 'foo') + def test_make_file_name(self): loader = Loader() @@ -67,3 +71,13 @@ class LoaderTestCase(unittest.TestCase): loader.template_extension = False self.assertEquals(loader.load_template('extensionless'), "No file extension: {{foo}}") + + def test_load_template__unicode_return_value(self): + """ + Check that load_template() returns unicode strings. + + """ + loader = Loader(search_dirs=self.search_dirs) + template = loader.load_template('simple') + + self.assertEqual(type(template), unicode) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index fd91f3c..41377ed 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -240,6 +240,20 @@ class RendererTestCase(unittest.TestCase): renderer.default_encoding = 'utf_8' self.assertEquals(renderer.render(template), "déf") + def test_make_load_partial__unicode(self): + """ + Test that the generated load_partial does not "double-decode" Unicode. + + """ + renderer = Renderer() + # In real-life, the partial would be different with each name. + renderer.load_template = lambda name: u"partial" + + load_partial = renderer._make_load_partial() + + # This would raise a TypeError exception if we tried to double-decode. + self.assertEquals(load_partial("test"), "partial") + # By testing that Renderer.render() constructs the RenderEngine instance # correctly, we no longer need to test the rendering code paths through # the Renderer. We can test rendering paths through only the RenderEngine -- cgit v1.2.1 From 9eab209c6f9c4197a79c70ff8c1dcf1f7785e45a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Dec 2011 00:24:20 -0800 Subject: Fixed issue #33 (via fczuardi): "multiline comments not working" See https://github.com/defunkt/pystache/issues/33 --- pystache/renderengine.py | 3 ++- tests/test_renderengine.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index b0b797c..c08dad1 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -106,7 +106,8 @@ class RenderEngine(object): self.section_re = re.compile(section, re.M|re.S) tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" % tags - self.tag_re = re.compile(tag) + # We use re.DOTALL to permit multiline comments, in accordance with the spec. + self.tag_re = re.compile(tag, re.DOTALL) def _render_tags(self, template): output = [] diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 2962cbe..ff8dd58 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -166,3 +166,11 @@ class RenderEngineTestCase(unittest.TestCase): context = {'test': (lambda text: '{{hi}} %s' % text)} self._assert_render('{{hi}} Mom', template, context) + def test_render__section__comment__multiline(self): + """ + Check that multiline comments are permitted. + + """ + self._assert_render('foobar', 'foo{{! baz }}bar') + self._assert_render('foobar', 'foo{{! \nbaz }}bar') + -- cgit v1.2.1 From 2d1d6f57fa49b736a7f800dc79ab0218c580153c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Dec 2011 00:33:42 -0800 Subject: Updated history notes for issue #33. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index e8c0fbb..597882e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,7 @@ Bug fixes: * Fixed an issue that affected the rendering of zeroes when using certain implementations of Python (i.e. PyPy). [alex] * Extensionless template files could not be loaded. [cjerdonek] +* Multline comments now permitted. [fczuardi] Misc: -- cgit v1.2.1 From 53ea24aab7ce7fcc6d752af8e3b141db0992a903 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Dec 2011 00:33:42 -0800 Subject: Updated history notes for issue #33. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index e8c0fbb..597882e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -29,6 +29,7 @@ Bug fixes: * Fixed an issue that affected the rendering of zeroes when using certain implementations of Python (i.e. PyPy). [alex] * Extensionless template files could not be loaded. [cjerdonek] +* Multline comments now permitted. [fczuardi] Misc: -- cgit v1.2.1 From ebb880d2faa1d66cb820bea0f4fa07fbf75fe815 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Dec 2011 17:48:15 -0800 Subject: Added "--with-doctest" to the nosetests instructions in the README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7642a47..88cb477 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ Python 2.6. Pystache is semantically versioned: http://semver.org. -Logo: David Phillips - http://davidphillips.us/ +Logo: David Phillips - http://davidphillips.us/ Documentation ============= @@ -65,7 +65,7 @@ nose_ works great! :: pip install nose cd pystache - nosetests + nosetests --with-doctest Mailing List -- cgit v1.2.1 From 6323e1cffae384553f42cec97324330d3fb995d4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Dec 2011 17:49:16 -0800 Subject: Corrected the modifiers doctest. --- pystache/renderengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index c08dad1..0c144a7 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -27,7 +27,7 @@ class Modifiers(dict): """ Return a decorator that assigns the given function to the given key. - >>> modifiers = {} + >>> modifiers = Modifiers() >>> @modifiers.set('P') ... def render_tongue(self, tag_name=None, context=None): ... return ":P %s" % tag_name -- cgit v1.2.1 From 17d62ebc02cbb859681a83a6a17ea4520685fb90 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 13:56:27 -0800 Subject: Changed the lone doctest to pass using "nosetests --with-doctest". Apparently, nosetests runs doctests without the doctest.ELLIPSIS option enabled. This caused the Modifiers.set() test to fail. --- pystache/renderengine.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 0c144a7..0cf7c74 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -28,11 +28,13 @@ class Modifiers(dict): Return a decorator that assigns the given function to the given key. >>> modifiers = Modifiers() + >>> >>> @modifiers.set('P') - ... def render_tongue(self, tag_name=None, context=None): - ... return ":P %s" % tag_name - >>> modifiers - {'P': } + ... def render_tongue(tag_name, context): + ... return "%s :P" % context.get(tag_name) + >>> + >>> modifiers['P']('text', {'text': 'hello!'}) + 'hello! :P' """ def decorate(func): -- cgit v1.2.1 From b0e78f9abb8d44069f1674bb549de5da8e93cc43 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 11:32:01 -0800 Subject: Change Loader.load_template() to Loader.get(). --- pystache/commands.py | 2 +- pystache/loader.py | 2 +- pystache/renderer.py | 5 ++--- pystache/view.py | 2 +- tests/test_loader.py | 22 +++++++++++----------- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index f2adf6e..dc17b88 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -55,7 +55,7 @@ def main(sys_argv): template = template[:-9] try: - template = Loader().load_template(template) + template = Loader().get(template) except IOError: pass diff --git a/pystache/loader.py b/pystache/loader.py index d92f168..84e1a3f 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -52,7 +52,7 @@ class Loader(object): return file_name - def load_template(self, template_name): + def get(self, template_name): """ Find and load the given template, and return it as a string. diff --git a/pystache/renderer.py b/pystache/renderer.py index 36cd4b5..9ad3ea1 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -22,7 +22,6 @@ except ImportError: class Renderer(object): - # TODO: change load_template to load_partial. def __init__(self, load_template=None, output_encoding=None, escape=None, default_encoding=None, decode_errors='strict'): """ @@ -33,7 +32,7 @@ class Renderer(object): load_template: a function for loading templates by name, for example when loading partials. The function should accept a single template_name parameter and return a template as a string. - Defaults to the default Loader's load_template() method. + Defaults to the default Loader's get() method. output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for @@ -65,7 +64,7 @@ class Renderer(object): """ if load_template is None: loader = Loader() - load_template = loader.load_template + load_template = loader.get if default_encoding is None: default_encoding = sys.getdefaultencoding() diff --git a/pystache/view.py b/pystache/view.py index f0dd335..7f37a87 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -51,7 +51,7 @@ class View(object): # View.__init__() has already been called. loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, extension=self.template_extension) - self._load_template = loader.load_template + self._load_template = loader.get return self._load_template(template_name) diff --git a/tests/test_loader.py b/tests/test_loader.py index 92c6471..9269a12 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -48,36 +48,36 @@ class LoaderTestCase(unittest.TestCase): loader.template_extension = '' self.assertEquals(loader.make_file_name('foo'), 'foo.') - def test_template_is_loaded(self): + def test_get__template_is_loaded(self): loader = Loader(search_dirs='examples') - template = loader.load_template('simple') + template = loader.get('simple') self.assertEqual(template, 'Hi {{thing}}!{{blank}}') - def test_using_list_of_paths(self): + def test_get__using_list_of_paths(self): loader = Loader(search_dirs=['doesnt_exist', 'examples']) - template = loader.load_template('simple') + template = loader.get('simple') self.assertEqual(template, 'Hi {{thing}}!{{blank}}') - def test_non_existent_template_fails(self): + def test_get__non_existent_template_fails(self): loader = Loader() - self.assertRaises(IOError, loader.load_template, 'doesnt_exist') + self.assertRaises(IOError, loader.get, 'doesnt_exist') - def test_load_template__extensionless_file(self): + def test_get__extensionless_file(self): loader = Loader(search_dirs=self.search_dirs) - self.assertRaises(IOError, loader.load_template, 'extensionless') + self.assertRaises(IOError, loader.get, 'extensionless') loader.template_extension = False - self.assertEquals(loader.load_template('extensionless'), "No file extension: {{foo}}") + self.assertEquals(loader.get('extensionless'), "No file extension: {{foo}}") - def test_load_template__unicode_return_value(self): + def test_get__load_template__unicode_return_value(self): """ Check that load_template() returns unicode strings. """ loader = Loader(search_dirs=self.search_dirs) - template = loader.load_template('simple') + template = loader.get('simple') self.assertEqual(type(template), unicode) -- cgit v1.2.1 From 28569a660a03c99b17e14f80bdb5f3d9c539fab4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 11:35:27 -0800 Subject: Moved load_template to end of Render.__init__() argument list. --- pystache/renderer.py | 22 +++++++++++----------- pystache/view.py | 3 ++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 9ad3ea1..ed426d3 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -22,18 +22,13 @@ except ImportError: class Renderer(object): - def __init__(self, load_template=None, output_encoding=None, escape=None, - default_encoding=None, decode_errors='strict'): + def __init__(self, output_encoding=None, escape=None, + default_encoding=None, decode_errors='strict', load_template=None): """ Construct an instance. Arguments: - load_template: a function for loading templates by name, for - example when loading partials. The function should accept a - single template_name parameter and return a template as a string. - Defaults to the default Loader's get() method. - output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for example "utf-8". See the render() method's documentation for more @@ -61,17 +56,22 @@ class Renderer(object): strings of type `str` encountered during the rendering process. Defaults to "strict". - """ - if load_template is None: - loader = Loader() - load_template = loader.get + load_template: a function for loading templates by name, for + example when loading partials. The function should accept a + single template_name parameter and return a template as a string. + Defaults to the default Loader's get() method. + """ if default_encoding is None: default_encoding = sys.getdefaultencoding() if escape is None: escape = markupsafe.escape if markupsafe else cgi.escape + if load_template is None: + loader = Loader() + load_template = loader.get + literal = markupsafe.Markup if markupsafe else unicode self._literal = literal diff --git a/pystache/view.py b/pystache/view.py index 7f37a87..226bb18 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -99,7 +99,8 @@ class View(object): """ template = self.get_template() - renderer = Renderer(self.load_template, output_encoding=encoding, escape=escape) + renderer = Renderer(output_encoding=encoding, escape=escape, + load_template=self.load_template) return renderer.render(template, self.context) def get(self, key, default=None): -- cgit v1.2.1 From 96af7f7251dee20304497d8b298bea45ac4909af Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 11:40:36 -0800 Subject: Improved Loader.__init__() docstring slightly. --- pystache/loader.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 84e1a3f..9dfa71c 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -19,22 +19,27 @@ class Loader(object): Arguments: + search_dirs: the list of directories in which to search for templates, + for example when looking for partials. Defaults to the current + working directory. If given a string, the string is interpreted + as a single directory. + + extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + encoding: the name of the encoding to use when converting file contents to unicode. This name will be passed as the encoding argument to the built-in function unicode(). Defaults to the encoding name returned by sys.getdefaultencoding(). - search_dirs: the directories in which to search for templates. - Defaults to the current working directory. - - extension: the template file extension. Defaults to "mustache". - Pass False for no extension. """ if encoding is None: encoding = sys.getdefaultencoding() + if extension is None: extension = DEFAULT_EXTENSION + if search_dirs is None: search_dirs = os.curdir # i.e. "." -- cgit v1.2.1 From 0b41209da23ea3be83b4cd3a9dd4f3f4614f7534 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 13:36:45 -0800 Subject: Improved the Renderer.render() docstring. --- pystache/renderer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index ed426d3..61ce40f 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -167,19 +167,19 @@ class Renderer(object): """ Render the given template using the given context. - The return value is a unicode string, unless the output_encoding - attribute has been set to a non-None value, in which case the - return value has type str and is encoded using that encoding. + Returns: - If the template string is not unicode, it is first converted to - unicode using the default_encoding and decode_errors attributes. - See the Template constructor's docstring for more information. + If the output_encoding attribute is None, the return value is + a unicode string. Otherwise, the return value is encoded to a + string of type str using the output encoding named by the + output_encoding attribute. Arguments: - template: a template string that is either unicode, or of type - str and encoded using the encoding named by the default_encoding - keyword argument. + template: a template string that is either unicode or of type str. + If the string has type str, it is first converted to unicode + using the default_encoding and decode_errors attributes of this + instance. See the constructor docstring for more information. context: a dictionary, Context, or object (e.g. a View instance). -- cgit v1.2.1 From abe5700e7b1319fe1c1bc7bb235a401b2f58545d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 14:03:14 -0800 Subject: Adjusted the Render.__init__() docstring. --- pystache/renderer.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 61ce40f..2d8d595 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -31,19 +31,20 @@ class Renderer(object): output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for - example "utf-8". See the render() method's documentation for more - information. + example "utf-8". See the render() method's documentation for + more information. escape: the function used to escape mustache variable values - when rendering a template. The function should accept a unicode - string and return an escaped string of the same type. It need - not handle strings of type `str` because this class will only - pass it unicode strings. The constructor assigns this escape - function to the constructed instance's Template.escape() method. - - The argument defaults to markupsafe.escape when markupsafe is - importable and cgi.escape otherwise. To disable escaping entirely, - one can pass `lambda s: s` as the escape function, for example. + when rendering a template. The function should accept a + unicode string and return an escaped string of the same type. + This function need not handle strings of type `str` because + this class will only pass it unicode strings. The constructor + assigns this function to the constructed instance's escape() + method. + The argument defaults to markupsafe.escape when markupsafe + is importable and cgi.escape otherwise. To disable escaping + entirely, one can pass `lambda u: u` as the escape function, + for example. default_encoding: the name of the encoding to use when converting to unicode any strings of type `str` encountered during the -- cgit v1.2.1 From d41815100cb864851dc21756bb326217898bcf89 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 16:02:47 -0800 Subject: Finished issue #64: "Change Renderer to accept a loader implementing get()" --- pystache/renderer.py | 54 +++++++++++++++++++++++++++++------------ pystache/view.py | 38 +++++++++++++++++++---------- tests/test_renderer.py | 66 +++++++++++++++++++++++++++++++++++++++++++------- tests/test_view.py | 10 +++----- 4 files changed, 124 insertions(+), 44 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 2d8d595..5939a85 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -22,13 +22,41 @@ except ImportError: class Renderer(object): - def __init__(self, output_encoding=None, escape=None, - default_encoding=None, decode_errors='strict', load_template=None): + """ + A class for rendering mustache templates. + + This class supports several rendering options which are described in + the constructor's docstring. Among these, the constructor supports + passing a custom template loader. + + Here is an example of passing a custom template loader to render a + template using partials loaded from a string-string dictionary. + + >>> partials = {'partial': 'Hello, {{thing}}!'} + >>> renderer = Renderer(loader=partials) + >>> renderer.render('{{>partial}}', {'thing': 'world'}) + u'Hello, world!' + + """ + + def __init__(self, loader=None, default_encoding=None, decode_errors='strict', + output_encoding=None, escape=None): """ Construct an instance. Arguments: + loader: the object (e.g. pystache.Loader or dictionary) that will + load templates during the rendering process, for example when + loading a partial. + The loader should have a get() method that accepts a string + and returns the corresponding template as a string, preferably + as a unicode string. If there is no template with that name, + the method should either return None (as dict.get() does) or + raise an exception. + Defaults to constructing a Loader instance with + default_encoding passed as the encoding argument. + output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for example "utf-8". See the render() method's documentation for @@ -57,11 +85,6 @@ class Renderer(object): strings of type `str` encountered during the rendering process. Defaults to "strict". - load_template: a function for loading templates by name, for - example when loading partials. The function should accept a - single template_name parameter and return a template as a string. - Defaults to the default Loader's get() method. - """ if default_encoding is None: default_encoding = sys.getdefaultencoding() @@ -69,9 +92,8 @@ class Renderer(object): if escape is None: escape = markupsafe.escape if markupsafe else cgi.escape - if load_template is None: - loader = Loader() - load_template = loader.get + if loader is None: + loader = Loader(encoding=default_encoding) literal = markupsafe.Markup if markupsafe else unicode @@ -80,7 +102,7 @@ class Renderer(object): self.decode_errors = decode_errors self.default_encoding = default_encoding self.escape = escape - self.load_template = load_template + self.loader = loader self.output_encoding = output_encoding def _unicode_and_escape(self, s): @@ -139,11 +161,11 @@ class Renderer(object): """ def load_partial(name): - template = self.load_template(name) - # Make sure the return value of load_template is unicode since - # RenderEngine requires it. Also, check that the string is not - # already unicode to avoid "double-decoding". Otherwise, we - # would get the following error: + template = self.loader.get(name) + # Make sure the return value is unicode since RenderEngine requires + # it. Also, check that the string is not already unicode to + # avoid "double-decoding". Otherwise, we would get the following + # error: # TypeError: decoding Unicode is not supported if not isinstance(template, unicode): template = self.unicode(template) diff --git a/pystache/view.py b/pystache/view.py index 226bb18..45cbd80 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -21,16 +21,24 @@ class View(object): template_encoding = None template_extension = None - # A function that accepts a single template_name parameter. - _load_template = None + _loader = None - def __init__(self, template=None, context=None, load_template=None, **kwargs): + def __init__(self, template=None, context=None, loader=None, **kwargs): """ Construct a View instance. + Arguments: + + loader: the object (e.g. pystache.Loader or dictionary) responsible + for loading templates during the rendering process, for example + when loading partials. The object should have a get() method + that accepts a string and returns the corresponding template + as a string, preferably as a unicode string. The method should + return None if there is no template with that name. + """ - if load_template is not None: - self._load_template = load_template + if loader is not None: + self._loader = loader if template is not None: self.template = template @@ -43,17 +51,21 @@ class View(object): self.context = _context - def load_template(self, template_name): - if self._load_template is None: - # We delay setting self._load_template until now (in the case - # that the user did not supply a load_template to the constructor) + def get_loader(self): + if self._loader is None: + # We delay setting self._loader until now (in the case that the + # user did not supply a load_template to the constructor) # to let users set the template_extension attribute, etc. after # View.__init__() has already been called. loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, extension=self.template_extension) - self._load_template = loader.get + self._loader = loader - return self._load_template(template_name) + return self._loader + + def load_template(self, template_name): + loader = self.get_loader() + return loader.get(template_name) def get_template(self): """ @@ -98,9 +110,9 @@ class View(object): Return the view rendered using the current context. """ + loader = self.get_loader() template = self.get_template() - renderer = Renderer(output_encoding=encoding, escape=escape, - load_template=self.load_template) + renderer = Renderer(output_encoding=encoding, escape=escape, loader=loader) return renderer.render(template, self.context) def get(self, key, default=None): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 41377ed..3fd42b3 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -11,6 +11,37 @@ import unittest from pystache import renderer from pystache.renderer import Renderer +from pystache.loader import Loader + +class RendererInitTestCase(unittest.TestCase): + + """A class to test the Renderer.__init__() method.""" + + def test_loader(self): + """Test that the loader attribute is set correctly.""" + loader = {'foo': 'bar'} + r = Renderer(loader=loader) + self.assertEquals(r.loader, {'foo': 'bar'}) + + def test_loader__default(self): + """Test that the default loader is constructed correctly.""" + r = Renderer() + actual = r.loader + + expected = Loader() + + self.assertEquals(type(actual), type(expected)) + self.assertEquals(actual.__dict__, expected.__dict__) + + def test_loader__default__default_encoding(self): + """Test that the default loader inherits the default_encoding.""" + r = Renderer(default_encoding='foo') + actual = r.loader + + expected = Loader(encoding='foo') + self.assertEquals(actual.template_encoding, expected.template_encoding) + # Check all attributes for good measure. + self.assertEquals(actual.__dict__, expected.__dict__) class RendererTestCase(unittest.TestCase): @@ -240,19 +271,37 @@ class RendererTestCase(unittest.TestCase): renderer.default_encoding = 'utf_8' self.assertEquals(renderer.render(template), "déf") + def test_make_load_partial(self): + """ + Test the _make_load_partial() method. + + """ + partials = {'foo': 'bar'} + renderer = Renderer(loader=partials) + load_partial = renderer._make_load_partial() + + actual = load_partial('foo') + self.assertEquals(actual, 'bar') + self.assertEquals(type(actual), unicode, "RenderEngine requires that " + "load_partial return unicode strings.") + def test_make_load_partial__unicode(self): """ - Test that the generated load_partial does not "double-decode" Unicode. + Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode. """ renderer = Renderer() - # In real-life, the partial would be different with each name. - renderer.load_template = lambda name: u"partial" + renderer.loader = {'partial': 'foo'} load_partial = renderer._make_load_partial() + self.assertEquals(load_partial("partial"), "foo") - # This would raise a TypeError exception if we tried to double-decode. - self.assertEquals(load_partial("test"), "partial") + # Now with a value that is already unicode. + renderer.loader = {'partial': u'foo'} + load_partial = renderer._make_load_partial() + # If the next line failed, we would get the following error: + # TypeError: decoding Unicode is not supported + self.assertEquals(load_partial("partial"), "foo") # By testing that Renderer.render() constructs the RenderEngine instance # correctly, we no longer need to test the rendering code paths through @@ -263,14 +312,13 @@ class RendererTestCase(unittest.TestCase): Test that _make_render_engine() constructs and passes load_partial correctly. """ - renderer = Renderer() + partials = {'partial': 'foo'} + renderer = Renderer(loader=partials) renderer.unicode = lambda s: s.upper() # a test version. - # In real-life, the partial would be different with each name. - renderer.load_template = lambda name: "partial" engine = renderer._make_render_engine() # Make sure it calls unicode. - self.assertEquals(engine.load_partial('name'), "PARTIAL") + self.assertEquals(engine.load_partial('partial'), "FOO") def test_make_render_engine__literal(self): """ diff --git a/tests/test_view.py b/tests/test_view.py index 0402f6d..bd2b616 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -60,15 +60,13 @@ class ViewTestCase(unittest.TestCase): template = view.load_template('extensionless') self.assertEquals(template, "No file extension: {{foo}}") - def test_custom_load_template(self): + def test_load_template__custom_loader(self): """ - Test passing a custom load_template to View.__init__(). + Test passing a custom loader to View.__init__(). """ - partials_dict = {"partial": "Loaded from dictionary"} - load_template = lambda template_name: partials_dict[template_name] - - view = Simple(load_template=load_template) + partials = {"partial": "Loaded from dictionary"} + view = Simple(loader=partials) actual = view.load_template("partial") self.assertEquals(actual, "Loaded from dictionary") -- cgit v1.2.1 From f8f8cc9330e4f8e6935151c0b0b748b873413eb7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 16:07:43 -0800 Subject: Updated history notes for issue #64. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 597882e..d8527a0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ API changes: * ``Template.render()`` now accepts the context to render instead of ``Template()``. [cjerdonek] +* ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek] Bug fixes: -- cgit v1.2.1 From 10e867c92393089b74113ab7053cc00137fb22ce Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 16:14:23 -0800 Subject: Updates to the history file. --- HISTORY.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d8527a0..297902b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,7 +6,8 @@ Next Release (version TBD) Features: -* A custom template loader can now be passed to a View. [cjerdonek] +* Views and Renderers accept a custom template loader. Also, this loader + can be a dictionary of partials. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] * Markupsafe can now be disabled after import. [cjerdonek] * Custom escape function can now be passed to Template constructor. [cjerdonek] @@ -16,8 +17,7 @@ Features: API changes: -* ``Template.render()`` now accepts the context to render instead of - ``Template()``. [cjerdonek] +* Template class replaced by a Renderer class. [cjerdonek] * ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek] Bug fixes: -- cgit v1.2.1 From 3a960197fa06f5c912b7651eac61431877f1bc67 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 16:53:50 -0800 Subject: Fixed issue #65: "Loader should accept decode_errors like Renderer" --- pystache/loader.py | 12 ++++++-- tests/data/ascii.mustache | 1 + tests/data/nonascii.mustache | 1 + tests/test_loader.py | 70 ++++++++++++++++++++++++++++++++++++-------- 4 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 tests/data/ascii.mustache create mode 100644 tests/data/nonascii.mustache diff --git a/pystache/loader.py b/pystache/loader.py index 9dfa71c..4647bbb 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -8,12 +8,13 @@ This module provides a Loader class. import os import sys +DEFAULT_DECODE_ERRORS = 'strict' DEFAULT_EXTENSION = 'mustache' class Loader(object): - def __init__(self, search_dirs=None, encoding=None, extension=None): + def __init__(self, search_dirs=None, extension=None, encoding=None, decode_errors=None): """ Construct a template loader. @@ -32,8 +33,14 @@ class Loader(object): argument to the built-in function unicode(). Defaults to the encoding name returned by sys.getdefaultencoding(). + decode_errors: the string to pass as the "errors" argument to the + built-in function unicode() when converting file contents to + unicode. Defaults to "strict". """ + if decode_errors is None: + decode_errors = DEFAULT_DECODE_ERRORS + if encoding is None: encoding = sys.getdefaultencoding() @@ -46,6 +53,7 @@ class Loader(object): if isinstance(search_dirs, basestring): search_dirs = [search_dirs] + self.decode_errors = decode_errors self.search_dirs = search_dirs self.template_encoding = encoding self.template_extension = extension @@ -88,6 +96,6 @@ class Loader(object): finally: f.close() - template = unicode(template, self.template_encoding) + template = unicode(template, self.template_encoding, self.decode_errors) return template diff --git a/tests/data/ascii.mustache b/tests/data/ascii.mustache new file mode 100644 index 0000000..e86737b --- /dev/null +++ b/tests/data/ascii.mustache @@ -0,0 +1 @@ +ascii: abc \ No newline at end of file diff --git a/tests/data/nonascii.mustache b/tests/data/nonascii.mustache new file mode 100644 index 0000000..bd69b61 --- /dev/null +++ b/tests/data/nonascii.mustache @@ -0,0 +1 @@ +non-ascii: é \ No newline at end of file diff --git a/tests/test_loader.py b/tests/test_loader.py index 9269a12..49c4348 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,14 +1,20 @@ +# encoding: utf-8 + import os import sys import unittest from pystache.loader import Loader +DATA_DIR = 'tests/data' class LoaderTestCase(unittest.TestCase): search_dirs = 'examples' + def _loader(self): + return Loader(search_dirs=DATA_DIR) + def test_init__search_dirs(self): # Test the default value. loader = Loader() @@ -17,18 +23,15 @@ class LoaderTestCase(unittest.TestCase): loader = Loader(search_dirs=['foo']) self.assertEquals(loader.search_dirs, ['foo']) - def test_init__extension(self): + def test_init__decode_errors(self): # Test the default value. loader = Loader() - self.assertEquals(loader.template_extension, 'mustache') + self.assertEquals(loader.decode_errors, 'strict') - loader = Loader(extension='txt') - self.assertEquals(loader.template_extension, 'txt') - - loader = Loader(extension=False) - self.assertTrue(loader.template_extension is False) + loader = Loader(decode_errors='replace') + self.assertEquals(loader.decode_errors, 'replace') - def test_init__loader(self): + def test_init__encoding(self): # Test the default value. loader = Loader() self.assertEquals(loader.template_encoding, sys.getdefaultencoding()) @@ -36,6 +39,17 @@ class LoaderTestCase(unittest.TestCase): loader = Loader(encoding='foo') self.assertEquals(loader.template_encoding, 'foo') + def test_init__extension(self): + # Test the default value. + loader = Loader() + self.assertEquals(loader.template_extension, 'mustache') + + loader = Loader(extension='txt') + self.assertEquals(loader.template_extension, 'txt') + + loader = Loader(extension=False) + self.assertTrue(loader.template_extension is False) + def test_make_file_name(self): loader = Loader() @@ -72,12 +86,42 @@ class LoaderTestCase(unittest.TestCase): loader.template_extension = False self.assertEquals(loader.get('extensionless'), "No file extension: {{foo}}") - def test_get__load_template__unicode_return_value(self): + def test_get(self): """ - Check that load_template() returns unicode strings. + Test get(). """ - loader = Loader(search_dirs=self.search_dirs) - template = loader.get('simple') + loader = self._loader() + self.assertEquals(loader.get('ascii'), 'ascii: abc') + + def test_get__unicode_return_value(self): + """ + Test that get() returns unicode strings. + + """ + loader = self._loader() + actual = loader.get('ascii') + self.assertEqual(type(actual), unicode) + + def test_get__encoding(self): + """ + Test get(): encoding attribute respected. + + """ + loader = self._loader() + + self.assertRaises(UnicodeDecodeError, loader.get, 'nonascii') + loader.template_encoding = 'utf-8' + self.assertEquals(loader.get('nonascii'), u'non-ascii: é') + + def test_get__decode_errors(self): + """ + Test get(): decode_errors attribute. + + """ + loader = self._loader() + + self.assertRaises(UnicodeDecodeError, loader.get, 'nonascii') + loader.decode_errors = 'replace' + self.assertEquals(loader.get('nonascii'), u'non-ascii: \ufffd\ufffd') - self.assertEqual(type(template), unicode) -- cgit v1.2.1 From 79cc6c94c1f6eb1f946203f5822cc443cdd70e67 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 16:56:32 -0800 Subject: Updated history notes for issue #65. --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 297902b..2a44afe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,7 @@ Features: * Template class can now handle non-ascii characters in non-unicode strings. Added default_encoding and decode_errors to Template constructor arguments. [cjerdonek] +* Loader supports a decode_errors argument. [cjerdonek] API changes: -- cgit v1.2.1 From a49fa8cb9ddc5240f3c0dc85cd1a7a3216bf7c01 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 17:03:19 -0800 Subject: The Renderer's default Loader now inherits the decode_errors argument. --- pystache/renderer.py | 5 +++-- tests/test_renderer.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 5939a85..a7a476e 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -55,7 +55,8 @@ class Renderer(object): the method should either return None (as dict.get() does) or raise an exception. Defaults to constructing a Loader instance with - default_encoding passed as the encoding argument. + default_encoding and decode_errors passed as the encoding and + decode_errors arguments, respectively. output_encoding: the encoding to use when rendering to a string. The argument should be the name of an encoding as a string, for @@ -93,7 +94,7 @@ class Renderer(object): escape = markupsafe.escape if markupsafe else cgi.escape if loader is None: - loader = Loader(encoding=default_encoding) + loader = Loader(encoding=default_encoding, decode_errors=decode_errors) literal = markupsafe.Markup if markupsafe else unicode diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 3fd42b3..c24647b 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -43,6 +43,17 @@ class RendererInitTestCase(unittest.TestCase): # Check all attributes for good measure. self.assertEquals(actual.__dict__, expected.__dict__) + def test_loader__default__decode_errors(self): + """Test that the default loader inherits the decode_errors.""" + r = Renderer(decode_errors='foo') + actual = r.loader + + expected = Loader(decode_errors='foo') + self.assertEquals(actual.decode_errors, expected.decode_errors) + # Check all attributes for good measure. + self.assertEquals(actual.__dict__, expected.__dict__) + + class RendererTestCase(unittest.TestCase): -- cgit v1.2.1 From f5e9950b0f06a99498e4050e323e560f6d74d513 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 21:06:59 -0800 Subject: Added a test case for issue #15: (non-list) "non-false value" --- tests/test_pystache.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index 012f45f..446c42c 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -15,6 +15,10 @@ class PystacheTests(object): """ + def _assert_rendered(self, expected, template, context): + actual = pystache.render(template, context) + self.assertEquals(actual, expected) + def test_basic(self): ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) self.assertEquals(ret, "Hi world!") @@ -94,6 +98,20 @@ class PystacheTests(object): ret = pystache.render(template, {"spacing": True}) self.assertEquals(ret, "first second third") + def test__section__non_false_value(self): + """ + Test when a section value is a (non-list) "non-false value". + + From mustache(5): + + When the value [of a section key] is non-false but not a list, it + will be used as the context for a single rendering of the block. + + """ + template = """{{#person}}Hi {{name}}{{/person}}""" + context = {"person": {"name": "Jon"}} + self._assert_rendered("Hi Jon", template, context) + def test_later_list_section_with_escapable_character(self): """ This is a simple test case intended to cover issue #53. -- cgit v1.2.1 From d13a4c43bf6e56e08f24c53d60f7f2acf6316c6f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Dec 2011 21:18:00 -0800 Subject: Refactoring: removed some of the cut-and-paste from test_pystache.py. --- tests/test_pystache.py | 53 ++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index 446c42c..f5e6b60 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -29,38 +29,38 @@ class PystacheTests(object): def test_less_basic(self): template = "It's a nice day for {{beverage}}, right {{person}}?" - ret = pystache.render(template, { 'beverage': 'soda', 'person': 'Bob' }) - self.assertEquals(ret, "It's a nice day for soda, right Bob?") + context = { 'beverage': 'soda', 'person': 'Bob' } + self._assert_rendered("It's a nice day for soda, right Bob?", template, context) def test_even_less_basic(self): template = "I think {{name}} wants a {{thing}}, right {{name}}?" - ret = pystache.render(template, { 'name': 'Jon', 'thing': 'racecar' }) - self.assertEquals(ret, "I think Jon wants a racecar, right Jon?") + context = { 'name': 'Jon', 'thing': 'racecar' } + self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context) def test_ignores_misses(self): template = "I think {{name}} wants a {{thing}}, right {{name}}?" - ret = pystache.render(template, { 'name': 'Jon' }) - self.assertEquals(ret, "I think Jon wants a , right Jon?") + context = { 'name': 'Jon' } + self._assert_rendered("I think Jon wants a , right Jon?", template, context) def test_render_zero(self): template = 'My value is {{value}}.' - ret = pystache.render(template, { 'value': 0 }) - self.assertEquals(ret, 'My value is 0.') + context = { 'value': 0 } + self._assert_rendered('My value is 0.', template, context) def test_comments(self): template = "What {{! the }} what?" - ret = pystache.render(template) - self.assertEquals(ret, "What what?") + actual = pystache.render(template) + self.assertEquals("What what?", actual) def test_false_sections_are_hidden(self): template = "Ready {{#set}}set {{/set}}go!" - ret = pystache.render(template, { 'set': False }) - self.assertEquals(ret, "Ready go!") + context = { 'set': False } + self._assert_rendered("Ready go!", template, context) def test_true_sections_are_shown(self): template = "Ready {{#set}}set{{/set}} go!" - ret = pystache.render(template, { 'set': True }) - self.assertEquals(ret, "Ready set go!") + context = { 'set': True } + self._assert_rendered("Ready set go!", template, context) non_strings_expected = """(123 & ['something'])(chris & 0.9)""" @@ -69,34 +69,32 @@ class PystacheTests(object): stats = [] stats.append({'key': 123, 'value': ['something']}) stats.append({'key': u"chris", 'value': 0.900}) - - ret = pystache.render(template, { 'stats': stats }) - self.assertEquals(ret, self.non_strings_expected) + context = { 'stats': stats } + self._assert_rendered(self.non_strings_expected, template, context) def test_unicode(self): template = 'Name: {{name}}; Age: {{age}}' - ret = pystache.render(template, { 'name': u'Henri Poincaré', - 'age': 156 }) - self.assertEquals(ret, u'Name: Henri Poincaré; Age: 156') + context = {'name': u'Henri Poincaré', 'age': 156 } + self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context) def test_sections(self): template = """
    {{#users}}
  • {{name}}
  • {{/users}}
""" context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } - ret = pystache.render(template, context) - self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") + expected = """
  • Chris
  • Tom
  • PJ
""" + self._assert_rendered(expected, template, context) def test_implicit_iterator(self): template = """
    {{#users}}
  • {{.}}
  • {{/users}}
""" context = { 'users': [ 'Chris', 'Tom','PJ' ] } - ret = pystache.render(template, context) - self.assertEquals(ret, """
  • Chris
  • Tom
  • PJ
""") + expected = """
  • Chris
  • Tom
  • PJ
""" + self._assert_rendered(expected, template, context) # The spec says that sections should not alter surrounding whitespace. def test_surrounding_whitepace_not_altered(self): template = "first{{#spacing}} second {{/spacing}}third" - ret = pystache.render(template, {"spacing": True}) - self.assertEquals(ret, "first second third") + context = {"spacing": True} + self._assert_rendered("first second third", template, context) def test__section__non_false_value(self): """ @@ -123,8 +121,7 @@ class PystacheTests(object): """ template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}""" context = {'s1': True, 's2': [True]} - actual = pystache.render(template, context) - self.assertEquals(actual, """foo <""") + self._assert_rendered("foo <", template, context) class PystacheWithoutMarkupsafeTests(PystacheTests, unittest.TestCase): -- cgit v1.2.1 From c50f77b87933d6cb40ffbbe703813e7a06025915 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Dec 2011 00:20:21 -0800 Subject: Added a doctest for issue #11: "View executes callables wrongly" --- pystache/context.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pystache/context.py b/pystache/context.py index 36140c7..d585f3f 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -125,6 +125,34 @@ class Context(object): (3) If there is no attribute with the same name as the key, then the key is considered not found in the item. + *Caution*: + + Callables resulting from a call to __getitem__ (as in (1) + above) are handled differently from callables that are merely + attributes (as in (2) above). + + The former are returned as-is, while the latter are first called + and that value returned. Here is an example: + + >>> def greet(): + ... return "Hi Bob!" + >>> + >>> class Greeter(object): + ... greet = None + >>> + >>> obj = Greeter() + >>> obj.greet = greet + >>> dct = {'greet': greet} + >>> + >>> obj.greet is dct['greet'] + True + >>> Context(obj).get('greet') + 'Hi Bob!' + >>> Context(dct).get('greet') #doctest: +ELLIPSIS + + + TODO: explain the rationale for this difference in treatment. + """ for obj in reversed(self._stack): val = _get_item(obj, key) -- cgit v1.2.1 From 59283fcbb0289ada55220a5d4fbbd3f2e3490754 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Dec 2011 00:29:25 -0800 Subject: Added a unit test case for issue #11. We added a case to check that callable return values of __getitem__ are not called. There is already a test that callable attributes are called: test_object__attribute_is_callable(). --- tests/test_context.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_context.py b/tests/test_context.py index dd25a5e..12bbff6 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -69,6 +69,18 @@ class GetItemTestCase(TestCase): obj = {"foo": "bar"} self.assertEquals(_get_item(obj, "foo"), "bar") + def test_dictionary__callable_not_called(self): + """ + Test that callable values are returned as-is (and in particular not called). + + """ + def foo_callable(self): + return "bar" + + obj = {"foo": foo_callable} + self.assertNotEquals(_get_item(obj, "foo"), "bar") + self.assertTrue(_get_item(obj, "foo") is foo_callable) + def test_dictionary__key_missing(self): """ Test getting a missing key from a dictionary. -- cgit v1.2.1 From bd304a1d2614c4204073581d2e5eb8a823fc0b36 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 06:12:25 -0800 Subject: Fix issue #66: "RenderEngine should operate only on unicode strings" Merry Christmas! :) --- pystache/renderengine.py | 119 +++++++++++++++--------- pystache/renderer.py | 104 +++++++++++---------- tests/test_renderengine.py | 150 +++++++++++++++++++++++------- tests/test_renderer.py | 223 +++++++++++++++++++++++++++++++++------------ 4 files changed, 413 insertions(+), 183 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 0cf7c74..f51bff5 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -50,6 +50,16 @@ class RenderEngine(object): This class is meant only for internal use by the Template class. + As a rule, the code in this class operates on unicode strings where + possible rather than, say, strings of type str or markupsafe.Markup. + This means that strings obtained from "external" sources like partials + and variable tag values are immediately converted to unicode (or + escaped and converted to unicode) before being operated on further. + This makes maintaining, reasoning about, and testing the correctness + of the code much simpler. In particular, it keeps the implementation + of this class independent of the API details of one (or possibly more) + unicode subclasses (e.g. markupsafe.Markup). + """ tag_re = None otag = '{{' @@ -61,15 +71,30 @@ class RenderEngine(object): """ Arguments: - load_partial: a function for loading templates by name when - loading partials. The function should accept a template name - and return a unicode template string. - - escape: a function that takes a unicode or str string, - converts it to unicode, and escapes and returns it. - - literal: a function that converts a unicode or str string - to unicode without escaping, and returns it. + load_partial: the function to call when loading a partial. The + function should accept a string template name and return a + template string of type unicode (not a subclass). + + literal: the function used to convert unescaped variable tag + values to unicode, e.g. the value corresponding to a tag + "{{{name}}}". The function should accept a string of type + str or unicode (or a subclass) and return a string of type + unicode (but not a proper subclass of unicode). + This class will only pass basestring instances to this + function. For example, it will call str() on integer variable + values prior to passing them to this function. + + escape: the function used to escape and convert variable tag + values to unicode, e.g. the value corresponding to a tag + "{{name}}". The function should obey the same properties + described above for the "literal" function argument. + This function should take care to convert any str + arguments to unicode just as the literal function should, as + this class will not pass tag values to literal prior to passing + them to this function. This allows for more flexibility, + for example using a custom escape function that handles + incoming strings of type markupssafe.Markup differently + from plain unicode strings. """ self.escape = escape @@ -78,9 +103,13 @@ class RenderEngine(object): def render(self, template, context): """ + Return a template rendered as a string with type unicode. + Arguments: - template: a unicode template string. + template: a template string of type unicode (but not a proper + subclass of unicode). + context: a Context instance. """ @@ -122,7 +151,7 @@ class RenderEngine(object): # Then there was no match. break - start, tag_type, tag_name, template = parts + tag_type, tag_name, template = parts[1:] tag_name = tag_name.strip() func = self.modifiers[tag_type] @@ -133,6 +162,7 @@ class RenderEngine(object): output.append(tag_value) output = "".join(output) + return output def _render_dictionary(self, template, context): @@ -149,35 +179,45 @@ class RenderEngine(object): return ''.join(insides) - @modifiers.set(None) - def _render_tag(self, tag_name): + def _get_string_context(self, tag_name): """ - Return the value of a variable as an escaped unicode string. + Get a value from the current context as a basestring instance. """ - raw = self.context.get(tag_name, '') + val = self.context.get(tag_name) - # For methods with no return value - # - # We use "==" rather than "is" to compare integers, as using "is" relies - # on an implementation detail of CPython. The test about rendering - # zeroes failed while using PyPy when using "is". + # We use "==" rather than "is" to compare integers, as using "is" + # relies on an implementation detail of CPython. The test about + # rendering zeroes failed while using PyPy when using "is". # See issue #34: https://github.com/defunkt/pystache/issues/34 - if not raw and raw != 0: - if tag_name == '.': - raw = self.context.top() - else: + if not val and val != 0: + if tag_name != '.': return '' + val = self.context.top() - # If we don't first convert to a string type, the call to self._unicode_and_escape() - # will yield an error like the following: - # - # TypeError: coercing to Unicode: need string or buffer, ... found - # - if not isinstance(raw, basestring): - raw = str(raw) + if not isinstance(val, basestring): + val = str(val) - return self.escape(raw) + return val + + @modifiers.set(None) + def _render_escaped(self, tag_name): + """ + Return a variable value as an escaped unicode string. + + """ + s = self._get_string_context(tag_name) + return self.escape(s) + + @modifiers.set('{') + @modifiers.set('&') + def _render_literal(self, tag_name): + """ + Return a variable value as a unicode string (unescaped). + + """ + s = self._get_string_context(tag_name) + return self.literal(s) @modifiers.set('!') def _render_comment(self, tag_name): @@ -185,8 +225,8 @@ class RenderEngine(object): @modifiers.set('>') def _render_partial(self, template_name): - markup = self.load_partial(template_name) - return self._render(markup) + template = self.load_partial(template_name) + return self._render(template) @modifiers.set('=') def _change_delimiter(self, tag_name): @@ -199,20 +239,11 @@ class RenderEngine(object): return '' - @modifiers.set('{') - @modifiers.set('&') - def _render_unescaped(self, tag_name): - """ - Render a tag without escaping it. - - """ - return self.literal(self.context.get(tag_name, '')) - def _render(self, template): """ Arguments: - template: a unicode template string. + template: a template string with type unicode. """ output = [] diff --git a/pystache/renderer.py b/pystache/renderer.py index a7a476e..391c214 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -63,9 +63,10 @@ class Renderer(object): example "utf-8". See the render() method's documentation for more information. - escape: the function used to escape mustache variable values - when rendering a template. The function should accept a - unicode string and return an escaped string of the same type. + escape: the function used to escape variable tag values when + rendering a template. The function should accept a unicode + string (or subclass of unicode) and return an escaped string + that is again unicode (or a subclass of unicode). This function need not handle strings of type `str` because this class will only pass it unicode strings. The constructor assigns this function to the constructed instance's escape() @@ -91,14 +92,13 @@ class Renderer(object): default_encoding = sys.getdefaultencoding() if escape is None: + # TODO: use 'quote=True' with cgi.escape and add tests. escape = markupsafe.escape if markupsafe else cgi.escape if loader is None: loader = Loader(encoding=default_encoding, decode_errors=decode_errors) - literal = markupsafe.Markup if markupsafe else unicode - - self._literal = literal + self._literal = markupsafe.Markup if markupsafe else unicode self.decode_errors = decode_errors self.default_encoding = default_encoding @@ -106,37 +106,46 @@ class Renderer(object): self.loader = loader self.output_encoding = output_encoding - def _unicode_and_escape(self, s): - if not isinstance(s, unicode): - s = self.unicode(s) - return self.escape(s) + def _to_unicode_soft(self, s): + """ + Convert an str or unicode string to a unicode string (or subclass). - def unicode(self, s): - return unicode(s, self.default_encoding, self.decode_errors) + """ + # Avoid the "double-decoding" TypeError. + return s if isinstance(s, unicode) else self.unicode(s) - def escape(self, u): + def _to_unicode_hard(self, s): """ - Escape a unicode string, and return it. + Convert an str or unicode string to a unicode string (not subclass). - This function is initialized as the escape function that was passed - to the Template class's constructor when this instance was - constructed. See the constructor docstring for more information. + """ + return unicode(self._to_unicode_soft(s)) + def _escape_to_unicode(self, s): """ - pass + Convert an str or unicode string to unicode, and escape it. + + Returns a unicode string (not subclass). - def literal(self, s): """ - Convert the given string to a unicode string, without escaping it. + return unicode(self.escape(self._to_unicode_soft(s))) - This function internally calls the built-in function unicode() and - passes it the default_encoding and decode_errors attributes for this - Template instance. If markupsafe was importable when loading this - module, this function returns an instance of the class - markupsafe.Markup (which subclasses unicode). + def unicode(self, s): + """ + Convert a string to unicode, using default_encoding and decode_errors. + + Raises: + + TypeError: Because this method calls Python's built-in unicode() + function, this method raises the following exception if the + given string is already unicode: + + TypeError: decoding Unicode is not supported """ - return self._literal(self.unicode(s)) + # TODO: Wrap UnicodeDecodeErrors with a message about setting + # the default_encoding and decode_errors attributes. + return unicode(s, self.default_encoding, self.decode_errors) def _make_context(self, context, **kwargs): """ @@ -157,21 +166,16 @@ class Renderer(object): return context def _make_load_partial(self): - """ - Return the load_partial function for use by RenderEngine. - - """ def load_partial(name): template = self.loader.get(name) - # Make sure the return value is unicode since RenderEngine requires - # it. Also, check that the string is not already unicode to - # avoid "double-decoding". Otherwise, we would get the following - # error: - # TypeError: decoding Unicode is not supported - if not isinstance(template, unicode): - template = self.unicode(template) - return template + if template is None: + # TODO: make a TemplateNotFoundException type that provides + # the original loader as an attribute. + raise Exception("Partial not found with name: %s" % repr(name)) + + # RenderEngine requires that the return value be unicode. + return self._to_unicode_hard(template) return load_partial @@ -183,8 +187,8 @@ class Renderer(object): load_partial = self._make_load_partial() engine = RenderEngine(load_partial=load_partial, - literal=self.literal, - escape=self._unicode_and_escape) + literal=self._to_unicode_hard, + escape=self._escape_to_unicode) return engine def render(self, template, context=None, **kwargs): @@ -194,30 +198,32 @@ class Renderer(object): Returns: If the output_encoding attribute is None, the return value is - a unicode string. Otherwise, the return value is encoded to a - string of type str using the output encoding named by the - output_encoding attribute. + markupsafe.Markup if markup was importable and unicode if not. + Otherwise, the return value is encoded to a string of type str + using the output encoding named by the output_encoding attribute. Arguments: template: a template string that is either unicode or of type str. If the string has type str, it is first converted to unicode - using the default_encoding and decode_errors attributes of this - instance. See the constructor docstring for more information. + using this instance's default_encoding and decode_errors + attributes. See the constructor docstring for more information. context: a dictionary, Context, or object (e.g. a View instance). - **kwargs: additional key values to add to the context when rendering. - These values take precedence over the context on any key conflicts. + **kwargs: additional key values to add to the context when + rendering. These values take precedence over the context on + any key conflicts. """ engine = self._make_render_engine() context = self._make_context(context, **kwargs) - if not isinstance(template, unicode): - template = self.unicode(template) + # RenderEngine.render() requires that the template string be unicode. + template = self._to_unicode_hard(template) rendered = engine.render(template, context) + rendered = self._literal(rendered) if self.output_encoding is not None: rendered = rendered.encode(self.output_encoding) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index ff8dd58..d2f4397 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -16,25 +16,42 @@ class RenderEngineTestCase(unittest.TestCase): """Test the RenderEngine class.""" - def _engine(self): + def test_init(self): """ - Create and return a default RenderEngine for testing. + Test that __init__() stores all of the arguments correctly. """ - to_unicode = unicode + # In real-life, these arguments would be functions + engine = RenderEngine(load_partial="foo", literal="literal", escape="escape") + + self.assertEquals(engine.escape, "escape") + self.assertEquals(engine.literal, "literal") + self.assertEquals(engine.load_partial, "foo") - escape = lambda s: cgi.escape(to_unicode(s)) - literal = to_unicode - engine = RenderEngine(literal=literal, escape=escape, load_partial=None) +class RenderEngineEnderTestCase(unittest.TestCase): + + """Test RenderEngine.render().""" + + def _engine(self): + """ + Create and return a default RenderEngine for testing. + + """ + escape = lambda s: unicode(cgi.escape(s)) + engine = RenderEngine(literal=unicode, escape=escape, load_partial=None) return engine def _assert_render(self, expected, template, *context, **kwargs): + """ + Test rendering the given template using the given context. + + """ partials = kwargs.get('partials') engine = kwargs.get('engine', self._engine()) if partials is not None: - engine.load_partial = lambda key: partials[key] + engine.load_partial = lambda key: unicode(partials[key]) context = Context(*context) @@ -42,22 +59,10 @@ class RenderEngineTestCase(unittest.TestCase): self.assertEquals(actual, expected) - def test_init(self): - """ - Test that __init__() stores all of the arguments correctly. - - """ - # In real-life, these arguments would be functions - engine = RenderEngine(load_partial="foo", literal="literal", escape="escape") - - self.assertEquals(engine.load_partial, "foo") - self.assertEquals(engine.escape, "escape") - self.assertEquals(engine.literal, "literal") - def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) - def test_render__load_partial(self): + def test__load_partial(self): """ Test that render() uses the load_template attribute. @@ -65,25 +70,106 @@ class RenderEngineTestCase(unittest.TestCase): engine = self._engine() partials = {'partial': "{{person}}"} engine.load_partial = lambda key: partials[key] + self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) - def test_render__literal(self): + def test__literal(self): """ Test that render() uses the literal attribute. """ engine = self._engine() engine.literal = lambda s: s.upper() - self._assert_render('bar BAR', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) - def test_render__escape(self): + self._assert_render('BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) + + def test__escape(self): """ Test that render() uses the escape attribute. """ engine = self._engine() engine.escape = lambda s: "**" + s - self._assert_render('**bar bar', '{{foo}} {{{foo}}}', {'foo': 'bar'}, engine=engine) + + self._assert_render('**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) + + def test__escape_does_not_call_literal(self): + """ + Test that render() does not call literal before or after calling escape. + + """ + engine = self._engine() + engine.literal = lambda s: s.upper() # a test version + engine.escape = lambda s: "**" + s + + template = 'literal: {{{foo}}} escaped: {{foo}}' + context = {'foo': 'bar'} + + self._assert_render('literal: BAR escaped: **bar', template, context, engine=engine) + + def test__escape_preserves_unicode_subclasses(self): + """ + Test that render() preserves unicode subclasses when passing to escape. + + This is useful, for example, if one wants to respect whether a + variable value is markupsafe.Markup when escaping. + + """ + class MyUnicode(unicode): + pass + + def escape(s): + if type(s) is MyUnicode: + return "**" + s + else: + return s + "**" + + engine = self._engine() + engine.escape = escape + + template = '{{foo1}} {{foo2}}' + context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'} + + self._assert_render('**bar bar**', template, context, engine=engine) + + def test__non_basestring__literal_and_escaped(self): + """ + Test a context value that is not a basestring instance. + + """ + # We use include upper() to make sure we are actually using + # our custom function in the tests + to_unicode = lambda s: unicode(s, encoding='ascii').upper() + engine = self._engine() + engine.escape = to_unicode + engine.literal = to_unicode + + self.assertRaises(TypeError, engine.literal, 100) + + template = '{{text}} {{int}} {{{int}}}' + context = {'int': 100, 'text': 'foo'} + + self._assert_render('FOO 100 100', template, context, engine=engine) + + def test__implicit_iterator__literal(self): + """ + Test an implicit iterator in a literal tag. + + """ + template = """{{#test}}{{.}}{{/test}}""" + context = {'test': ['a', 'b']} + + self._assert_render('ab', template, context) + + def test__implicit_iterator__escaped(self): + """ + Test an implicit iterator in a normal tag. + + """ + template = """{{#test}}{{{.}}}{{/test}}""" + context = {'test': ['a', 'b']} + + self._assert_render('ab', template, context) def test_render_with_partial(self): partials = {'partial': "{{person}}"} @@ -95,13 +181,11 @@ class RenderEngineTestCase(unittest.TestCase): """ engine = self._engine() - engine.escape = lambda s: "**" + s - engine.literal = lambda s: s.upper() - template = '{{#test}}{{foo}} {{{foo}}}{{/test}}' - context = {'test': {'foo': 'bar'}} + template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}' + context = {'test': {'foo': '<'}} - self._assert_render('**bar BAR', template, context, engine=engine) + self._assert_render('unescaped: < escaped: <', template, context, engine=engine) def test_render__partial_context_values(self): """ @@ -109,12 +193,12 @@ class RenderEngineTestCase(unittest.TestCase): """ engine = self._engine() - engine.escape = lambda s: "**" + s - engine.literal = lambda s: s.upper() - partials = {'partial': '{{foo}} {{{foo}}}'} + template = '{{>partial}}' + partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} + context = {'foo': '<'} - self._assert_render('**bar BAR', '{{>partial}}', {'foo': 'bar'}, engine=engine, partials=partials) + self._assert_render('unescaped: < escaped: <', template, context, engine=engine, partials=partials) def test_render__list_referencing_outer_context(self): """ diff --git a/tests/test_renderer.py b/tests/test_renderer.py index c24647b..0a82322 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -13,6 +13,7 @@ from pystache import renderer from pystache.renderer import Renderer from pystache.loader import Loader + class RendererInitTestCase(unittest.TestCase): """A class to test the Renderer.__init__() method.""" @@ -54,7 +55,6 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(actual.__dict__, expected.__dict__) - class RendererTestCase(unittest.TestCase): """Test the Renderer class.""" @@ -146,13 +146,13 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer(decode_errors="foo") self.assertEquals(renderer.decode_errors, "foo") - def test_unicode(self): - renderer = Renderer() - actual = renderer.literal("abc") - self.assertEquals(actual, "abc") - self.assertEquals(type(actual), unicode) + ## Test Renderer.unicode(). def test_unicode__default_encoding(self): + """ + Test that the default_encoding attribute is respected. + + """ renderer = Renderer() s = "é" @@ -163,40 +163,20 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(renderer.unicode(s), u"é") def test_unicode__decode_errors(self): - renderer = Renderer() - s = "é" + """ + Test that the decode_errors attribute is respected. + """ + renderer = Renderer() renderer.default_encoding = "ascii" - renderer.decode_errors = "strict" - self.assertRaises(UnicodeDecodeError, renderer.unicode, s) + s = "déf" + + renderer.decode_errors = "ignore" + self.assertEquals(renderer.unicode(s), "df") renderer.decode_errors = "replace" # U+FFFD is the official Unicode replacement character. - self.assertEquals(renderer.unicode(s), u'\ufffd\ufffd') - - def test_literal__with_markupsafe(self): - if not self._was_markupsafe_imported(): - # Then we cannot test this case. - return - self._restore_markupsafe() - - _renderer = Renderer() - _renderer.default_encoding = "utf_8" - - # Check the standard case. - actual = _renderer.literal("abc") - self.assertEquals(actual, "abc") - self.assertEquals(type(actual), renderer.markupsafe.Markup) - - s = "é" - # Check that markupsafe respects default_encoding. - self.assertEquals(_renderer.literal(s), u"é") - _renderer.default_encoding = "ascii" - self.assertRaises(UnicodeDecodeError, _renderer.literal, s) - - # Check that markupsafe respects decode_errors. - _renderer.decode_errors = "replace" - self.assertEquals(_renderer.literal(s), u'\ufffd\ufffd') + self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf') def test_render__unicode(self): renderer = Renderer() @@ -314,47 +294,176 @@ class RendererTestCase(unittest.TestCase): # TypeError: decoding Unicode is not supported self.assertEquals(load_partial("partial"), "foo") - # By testing that Renderer.render() constructs the RenderEngine instance - # correctly, we no longer need to test the rendering code paths through - # the Renderer. We can test rendering paths through only the RenderEngine - # for the same amount of code coverage. - def test_make_render_engine__load_partial(self): + +# By testing that Renderer.render() constructs the right RenderEngine, +# we no longer need to exercise all rendering code paths through +# the Renderer. It suffices to test rendering paths through the +# RenderEngine for the same amount of code coverage. +class Renderer_MakeRenderEngineTests(unittest.TestCase): + + """ + Check the RenderEngine returned by Renderer._make_render_engine(). + + """ + + ## Test the engine's load_partial attribute. + + def test__load_partial__returns_unicode(self): """ - Test that _make_render_engine() constructs and passes load_partial correctly. + Check that load_partial returns unicode (and not a subclass). """ - partials = {'partial': 'foo'} - renderer = Renderer(loader=partials) - renderer.unicode = lambda s: s.upper() # a test version. + class MyUnicode(unicode): + pass + + renderer = Renderer() + renderer.default_encoding = 'ascii' + renderer.loader = {'str': 'foo', 'subclass': MyUnicode('abc')} engine = renderer._make_render_engine() - # Make sure it calls unicode. - self.assertEquals(engine.load_partial('partial'), "FOO") - def test_make_render_engine__literal(self): + actual = engine.load_partial('str') + self.assertEquals(actual, "foo") + self.assertEquals(type(actual), unicode) + + # Check that unicode subclasses are not preserved. + actual = engine.load_partial('subclass') + self.assertEquals(actual, "abc") + self.assertEquals(type(actual), unicode) + + def test__load_partial__not_found(self): """ - Test that _make_render_engine() passes the right literal. + Check that load_partial provides a nice message when a template is not found. """ renderer = Renderer() - renderer.literal = "foo" # in real life, this would be a function. + renderer.loader = {} engine = renderer._make_render_engine() - self.assertEquals(engine.literal, "foo") + load_partial = engine.load_partial - def test_make_render_engine__escape(self): + try: + load_partial("foo") + raise Exception("Shouldn't get here") + except Exception, err: + self.assertEquals(str(err), "Partial not found with name: 'foo'") + + ## Test the engine's literal attribute. + + def test__literal__uses_renderer_unicode(self): """ - Test that _make_render_engine() passes the right escape. + Test that literal uses the renderer's unicode function. """ renderer = Renderer() - renderer.unicode = lambda s: s.upper() # a test version. - renderer.escape = lambda s: "**" + s # a test version. + renderer.unicode = lambda s: s.upper() + + engine = renderer._make_render_engine() + literal = engine.literal + + self.assertEquals(literal("foo"), "FOO") + + def test__literal__handles_unicode(self): + """ + Test that literal doesn't try to "double decode" unicode. + + """ + renderer = Renderer() + renderer.default_encoding = 'ascii' + + engine = renderer._make_render_engine() + literal = engine.literal + + self.assertEquals(literal(u"foo"), "foo") + + def test__literal__returns_unicode(self): + """ + Test that literal returns unicode (and not a subclass). + + """ + renderer = Renderer() + renderer.default_encoding = 'ascii' + + engine = renderer._make_render_engine() + literal = engine.literal + + self.assertEquals(type(literal("foo")), unicode) + + class MyUnicode(unicode): + pass + + s = MyUnicode("abc") + + self.assertEquals(type(s), MyUnicode) + self.assertTrue(isinstance(s, unicode)) + self.assertEquals(type(literal(s)), unicode) + + ## Test the engine's escape attribute. + + def test__escape__uses_renderer_escape(self): + """ + Test that escape uses the renderer's escape function. + + """ + renderer = Renderer() + renderer.escape = lambda s: "**" + s + + engine = renderer._make_render_engine() + escape = engine.escape + + self.assertEquals(escape("foo"), "**foo") + + def test__escape__uses_renderer_unicode(self): + """ + Test that escape uses the renderer's unicode function. + + """ + renderer = Renderer() + renderer.unicode = lambda s: s.upper() + + engine = renderer._make_render_engine() + escape = engine.escape + + self.assertEquals(escape("foo"), "FOO") + + def test__escape__has_access_to_original_unicode_subclass(self): + """ + Test that escape receives strings with the unicode subclass intact. + + """ + renderer = Renderer() + renderer.escape = lambda s: type(s).__name__ engine = renderer._make_render_engine() escape = engine.escape - self.assertEquals(escape(u"foo"), "**foo") + class MyUnicode(unicode): + pass + + self.assertEquals(escape("foo"), "unicode") + self.assertEquals(escape(u"foo"), "unicode") + self.assertEquals(escape(MyUnicode("foo")), "MyUnicode") + + def test__escape__returns_unicode(self): + """ + Test that literal returns unicode (and not a subclass). + + """ + renderer = Renderer() + renderer.default_encoding = 'ascii' + + engine = renderer._make_render_engine() + escape = engine.escape + + self.assertEquals(type(escape("foo")), unicode) + + # Check that literal doesn't preserve unicode subclasses. + class MyUnicode(unicode): + pass + + s = MyUnicode("abc") + + self.assertEquals(type(s), MyUnicode) + self.assertTrue(isinstance(s, unicode)) + self.assertEquals(type(escape(s)), unicode) - # Test that escape converts str strings to unicode first. - self.assertEquals(escape("foo"), "**FOO") -- cgit v1.2.1 From 4e9f0064c21faede11ecd845b7d66487c0379b73 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 06:39:01 -0800 Subject: Fixed issue #67: "Use cgi.escape() with quote=True" --- pystache/renderer.py | 15 ++++++++++----- tests/test_renderer.py | 8 ++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 391c214..a4bb995 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -92,8 +92,13 @@ class Renderer(object): default_encoding = sys.getdefaultencoding() if escape is None: - # TODO: use 'quote=True' with cgi.escape and add tests. - escape = markupsafe.escape if markupsafe else cgi.escape + if markupsafe: + escape = markupsafe.escape + else: + # The quote=True argument causes double quotes to be escaped, + # but not single quotes: + # http://docs.python.org/library/cgi.html#cgi.escape + escape = lambda s: cgi.escape(s, quote=True) if loader is None: loader = Loader(encoding=default_encoding, decode_errors=decode_errors) @@ -108,7 +113,7 @@ class Renderer(object): def _to_unicode_soft(self, s): """ - Convert an str or unicode string to a unicode string (or subclass). + Convert a basestring to unicode, preserving any unicode subclass. """ # Avoid the "double-decoding" TypeError. @@ -116,14 +121,14 @@ class Renderer(object): def _to_unicode_hard(self, s): """ - Convert an str or unicode string to a unicode string (not subclass). + Convert a basestring to a string with type unicode (not subclass). """ return unicode(self._to_unicode_soft(s)) def _escape_to_unicode(self, s): """ - Convert an str or unicode string to unicode, and escape it. + Convert a basestring to unicode (preserving any unicode subclass), and escape it. Returns a unicode string (not subclass). diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 0a82322..f913375 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -97,8 +97,12 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) def test_init__escape__default_without_markupsafe(self): - renderer = Renderer() - self.assertEquals(renderer.escape(">'"), ">'") + escape = Renderer().escape + + self.assertEquals(escape(">"), ">") + self.assertEquals(escape('"'), """) + # Single quotes are not escaped. + self.assertEquals(escape("'"), "'") def test_init__escape__default_with_markupsafe(self): if not self._was_markupsafe_imported(): -- cgit v1.2.1 From 8ad9282e83f84013b8582008f16bae6e14bb8818 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 06:57:55 -0800 Subject: Fixed issue #68: "Remove output_encoding option from Renderer, etc." --- HISTORY.rst | 1 + pystache/renderer.py | 19 +++---------------- pystache/view.py | 4 ++-- tests/test_examples.py | 3 --- tests/test_renderer.py | 11 ++--------- 5 files changed, 8 insertions(+), 30 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2a44afe..3e2f997 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -20,6 +20,7 @@ API changes: * Template class replaced by a Renderer class. [cjerdonek] * ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek] +* Removed output_encoding options. [cjerdonek] Bug fixes: diff --git a/pystache/renderer.py b/pystache/renderer.py index a4bb995..0016aa6 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -40,7 +40,7 @@ class Renderer(object): """ def __init__(self, loader=None, default_encoding=None, decode_errors='strict', - output_encoding=None, escape=None): + escape=None): """ Construct an instance. @@ -58,11 +58,6 @@ class Renderer(object): default_encoding and decode_errors passed as the encoding and decode_errors arguments, respectively. - output_encoding: the encoding to use when rendering to a string. - The argument should be the name of an encoding as a string, for - example "utf-8". See the render() method's documentation for - more information. - escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode string (or subclass of unicode) and return an escaped string @@ -109,7 +104,6 @@ class Renderer(object): self.default_encoding = default_encoding self.escape = escape self.loader = loader - self.output_encoding = output_encoding def _to_unicode_soft(self, s): """ @@ -200,12 +194,8 @@ class Renderer(object): """ Render the given template using the given context. - Returns: - - If the output_encoding attribute is None, the return value is - markupsafe.Markup if markup was importable and unicode if not. - Otherwise, the return value is encoded to a string of type str - using the output encoding named by the output_encoding attribute. + The return value is markupsafe.Markup if markup was importable + and unicode otherwise. Arguments: @@ -230,7 +220,4 @@ class Renderer(object): rendered = engine.render(template, context) rendered = self._literal(rendered) - if self.output_encoding is not None: - rendered = rendered.encode(self.output_encoding) - return rendered diff --git a/pystache/view.py b/pystache/view.py index 45cbd80..ca11f7c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -105,14 +105,14 @@ class View(object): # and options like encoding, escape, etc. This would probably be better # than passing all of these options to render(), especially as the list # of possible options grows. - def render(self, encoding=None, escape=None): + def render(self, escape=None): """ Return the view rendered using the current context. """ loader = self.get_loader() template = self.get_template() - renderer = Renderer(output_encoding=encoding, escape=escape, loader=loader) + renderer = Renderer(escape=escape, loader=loader) return renderer.render(template, self.context) def get(self, key, default=None): diff --git a/tests/test_examples.py b/tests/test_examples.py index 4abe673..58ea108 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -24,9 +24,6 @@ class TestView(unittest.TestCase): def test_unicode_output(self): self.assertEquals(UnicodeOutput().render(), u'

Name: Henri Poincaré

') - def test_encoded_output(self): - self.assertEquals(UnicodeOutput().render('utf8'), '

Name: Henri Poincar\xc3\xa9

') - def test_unicode_input(self): self.assertEquals(UnicodeInput().render(), u'

If alive today, Henri Poincaré would be 156 years old.

') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index f913375..9371277 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -243,19 +243,12 @@ class RendererTestCase(unittest.TestCase): renderer.render('Hi {{person}}', context=context, foo="bar") self.assertEquals(context, {}) - def test_render__output_encoding(self): - renderer = Renderer() - renderer.output_encoding = 'utf-8' - actual = renderer.render(u'Poincaré') - self.assertTrue(isinstance(actual, str)) - self.assertEquals(actual, 'Poincaré') - def test_render__nonascii_template(self): """ Test passing a non-unicode template with non-ascii characters. """ - renderer = Renderer(output_encoding="utf-8") + renderer = Renderer() template = "déf" # Check that decode_errors and default_encoding are both respected. @@ -264,7 +257,7 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(renderer.render(template), "df") renderer.default_encoding = 'utf_8' - self.assertEquals(renderer.render(template), "déf") + self.assertEquals(renderer.render(template), u"déf") def test_make_load_partial(self): """ -- cgit v1.2.1 From 39987aaf68189c82fc1060ca530590ccfa30a0a6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 07:52:46 -0800 Subject: Fixed issue #68: "remove markupsafe dependency" --- HISTORY.rst | 1 + pystache/renderer.py | 34 +++++---------- tests/test_pystache.py | 37 +--------------- tests/test_renderer.py | 114 ++++++++++++++++++++----------------------------- 4 files changed, 59 insertions(+), 127 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3e2f997..d867634 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,7 @@ API changes: * Template class replaced by a Renderer class. [cjerdonek] * ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek] * Removed output_encoding options. [cjerdonek] +* Removed automatic use of markupsafe, if available. [cjerdonek] Bug fixes: diff --git a/pystache/renderer.py b/pystache/renderer.py index 0016aa6..591e16e 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -13,13 +13,6 @@ from .loader import Loader from .renderengine import RenderEngine -markupsafe = None -try: - import markupsafe -except ImportError: - pass - - class Renderer(object): """ @@ -66,10 +59,10 @@ class Renderer(object): this class will only pass it unicode strings. The constructor assigns this function to the constructed instance's escape() method. - The argument defaults to markupsafe.escape when markupsafe - is importable and cgi.escape otherwise. To disable escaping - entirely, one can pass `lambda u: u` as the escape function, - for example. + The argument defaults to `cgi.escape(s, quote=True)`. To + disable escaping entirely, one can pass `lambda u: u` as the + escape function, for example. One may also wish to consider + using markupsafe's escape function: markupsafe.escape(). default_encoding: the name of the encoding to use when converting to unicode any strings of type `str` encountered during the @@ -87,19 +80,14 @@ class Renderer(object): default_encoding = sys.getdefaultencoding() if escape is None: - if markupsafe: - escape = markupsafe.escape - else: - # The quote=True argument causes double quotes to be escaped, - # but not single quotes: - # http://docs.python.org/library/cgi.html#cgi.escape - escape = lambda s: cgi.escape(s, quote=True) + # The quote=True argument causes double quotes to be escaped, + # but not single quotes: + # http://docs.python.org/library/cgi.html#cgi.escape + escape = lambda s: cgi.escape(s, quote=True) if loader is None: loader = Loader(encoding=default_encoding, decode_errors=decode_errors) - self._literal = markupsafe.Markup if markupsafe else unicode - self.decode_errors = decode_errors self.default_encoding = default_encoding self.escape = escape @@ -194,8 +182,7 @@ class Renderer(object): """ Render the given template using the given context. - The return value is markupsafe.Markup if markup was importable - and unicode otherwise. + Returns a unicode string. Arguments: @@ -218,6 +205,5 @@ class Renderer(object): template = self._to_unicode_hard(template) rendered = engine.render(template, context) - rendered = self._literal(rendered) - return rendered + return unicode(rendered) diff --git a/tests/test_pystache.py b/tests/test_pystache.py index f5e6b60..f9857cd 100644 --- a/tests/test_pystache.py +++ b/tests/test_pystache.py @@ -5,15 +5,7 @@ import pystache from pystache import renderer -class PystacheTests(object): - - """ - Contains tests to run with markupsafe both enabled and disabled. - - To run the tests in this class, this class should be subclassed by - a class that implements unittest.TestCase. - - """ +class PystacheTests(unittest.TestCase): def _assert_rendered(self, expected, template, context): actual = pystache.render(template, context) @@ -122,30 +114,3 @@ class PystacheTests(object): template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}""" context = {'s1': True, 's2': [True]} self._assert_rendered("foo <", template, context) - - -class PystacheWithoutMarkupsafeTests(PystacheTests, unittest.TestCase): - - """Test pystache without markupsafe enabled.""" - - def setUp(self): - self.original_markupsafe = renderer.markupsafe - renderer.markupsafe = None - - def tearDown(self): - renderer.markupsafe = self.original_markupsafe - - -# If markupsafe is available, then run the same tests again but without -# disabling markupsafe. -_BaseClass = unittest.TestCase if renderer.markupsafe else object -class PystacheWithMarkupsafeTests(PystacheTests, _BaseClass): - - """Test pystache with markupsafe enabled.""" - - # markupsafe.escape() escapes single quotes: "'" becomes "'". - non_strings_expected = """(123 & ['something'])(chris & 0.9)""" - - def test_markupsafe_available(self): - self.assertTrue(renderer.markupsafe, "markupsafe isn't available. " - "The with-markupsafe tests shouldn't be running.") diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 9371277..b6b3c65 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -16,16 +16,25 @@ from pystache.loader import Loader class RendererInitTestCase(unittest.TestCase): - """A class to test the Renderer.__init__() method.""" + """ + Tests the Renderer.__init__() method. + + """ def test_loader(self): - """Test that the loader attribute is set correctly.""" + """ + Test that the loader attribute is set correctly. + + """ loader = {'foo': 'bar'} r = Renderer(loader=loader) self.assertEquals(r.loader, {'foo': 'bar'}) def test_loader__default(self): - """Test that the default loader is constructed correctly.""" + """ + Test that the default loader is constructed correctly. + + """ r = Renderer() actual = r.loader @@ -35,7 +44,10 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(actual.__dict__, expected.__dict__) def test_loader__default__default_encoding(self): - """Test that the default loader inherits the default_encoding.""" + """ + Test that the default loader inherits the default_encoding. + + """ r = Renderer(default_encoding='foo') actual = r.loader @@ -45,7 +57,10 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(actual.__dict__, expected.__dict__) def test_loader__default__decode_errors(self): - """Test that the default loader inherits the decode_errors.""" + """ + Test that the default loader inherits the decode_errors. + + """ r = Renderer(decode_errors='foo') actual = r.loader @@ -54,49 +69,7 @@ class RendererInitTestCase(unittest.TestCase): # Check all attributes for good measure. self.assertEquals(actual.__dict__, expected.__dict__) - -class RendererTestCase(unittest.TestCase): - - """Test the Renderer class.""" - - def setUp(self): - """ - Disable markupsafe. - - """ - self.original_markupsafe = renderer.markupsafe - renderer.markupsafe = None - - def tearDown(self): - self._restore_markupsafe() - - def _renderer(self): - return Renderer() - - def _was_markupsafe_imported(self): - return bool(self.original_markupsafe) - - def _restore_markupsafe(self): - """ - Restore markupsafe to its original state. - - """ - renderer.markupsafe = self.original_markupsafe - - def test__was_markupsafe_imported(self): - """ - Test that our helper function works. - - """ - markupsafe = None - try: - import markupsafe - except: - pass - - self.assertEquals(bool(markupsafe), self._was_markupsafe_imported()) - - def test_init__escape__default_without_markupsafe(self): + def test_escape__default(self): escape = Renderer().escape self.assertEquals(escape(">"), ">") @@ -104,21 +77,12 @@ class RendererTestCase(unittest.TestCase): # Single quotes are not escaped. self.assertEquals(escape("'"), "'") - def test_init__escape__default_with_markupsafe(self): - if not self._was_markupsafe_imported(): - # Then we cannot test this case. - return - self._restore_markupsafe() - - renderer = Renderer() - self.assertEquals(renderer.escape(">'"), ">'") - - def test_init__escape(self): - escape = lambda s: "foo" + s + def test_escape(self): + escape = lambda s: "**" + s renderer = Renderer(escape=escape) - self.assertEquals(renderer.escape("bar"), "foobar") + self.assertEquals(renderer.escape("bar"), "**bar") - def test_init__default_encoding__default(self): + def test_default_encoding__default(self): """ Check the default value. @@ -126,7 +90,7 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer() self.assertEquals(renderer.default_encoding, sys.getdefaultencoding()) - def test_init__default_encoding(self): + def test_default_encoding(self): """ Check that the constructor sets the attribute correctly. @@ -134,7 +98,7 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer(default_encoding="foo") self.assertEquals(renderer.default_encoding, "foo") - def test_init__decode_errors__default(self): + def test_decode_errors__default(self): """ Check the default value. @@ -142,7 +106,7 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer() self.assertEquals(renderer.decode_errors, 'strict') - def test_init__decode_errors(self): + def test_decode_errors(self): """ Check that the constructor sets the attribute correctly. @@ -150,6 +114,14 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer(decode_errors="foo") self.assertEquals(renderer.decode_errors, "foo") + +class RendererTestCase(unittest.TestCase): + + """Test the Renderer class.""" + + def _renderer(self): + return Renderer() + ## Test Renderer.unicode(). def test_unicode__default_encoding(self): @@ -182,22 +154,30 @@ class RendererTestCase(unittest.TestCase): # U+FFFD is the official Unicode replacement character. self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf') + ## Test the render() method. + + def test_render__return_type(self): + """ + Check that render() returns a string of type unicode. + + """ + renderer = Renderer() + rendered = renderer.render('foo') + self.assertEquals(type(rendered), unicode) + def test_render__unicode(self): renderer = Renderer() actual = renderer.render(u'foo') - self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'foo') def test_render__str(self): renderer = Renderer() actual = renderer.render('foo') - self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, 'foo') def test_render__non_ascii_character(self): renderer = Renderer() actual = renderer.render(u'Poincaré') - self.assertTrue(isinstance(actual, unicode)) self.assertEquals(actual, u'Poincaré') def test_render__context(self): -- cgit v1.2.1 From 17ce2986530d089e7c8428621b35f5b75ba66b7e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 12:47:18 -0800 Subject: Added instructions to README on how to run the spec tests. --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 88cb477..4db8a03 100644 --- a/README.rst +++ b/README.rst @@ -67,6 +67,11 @@ nose_ works great! :: cd pystache nosetests --with-doctest +To include tests from the mustache spec_ in your test runs: :: + + git submodule init + git submodule update + Mailing List ================== @@ -90,4 +95,5 @@ Author .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html .. _Mustache: http://defunkt.github.com/mustache/ .. _mustache(5): http://mustache.github.com/mustache.5.html -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html \ No newline at end of file +.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html +.. _spec: https://github.com/mustache/spec -- cgit v1.2.1 From 4eaa43b5379e1c8e14e97b6661a90e026bcb3a57 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 14:01:33 -0800 Subject: Improved display format of spec tests and fixed partials typo. --- tests/test_spec.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/test_spec.py b/tests/test_spec.py index 22ad0c0..b51900a 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,9 +1,18 @@ +# coding: utf-8 + +""" +Tests the mustache spec test cases. + +""" + import glob import os.path -import pystache import unittest import yaml +from pystache.renderer import Renderer + + def code_constructor(loader, node): value = loader.construct_mapping(node) return eval(value['python'], {}) @@ -16,23 +25,41 @@ specs = glob.glob(os.path.join(specs, '*.yml')) class MustacheSpec(unittest.TestCase): pass -def buildTest(testData, spec): +def buildTest(testData, spec_filename): + + name = testData['name'] + description = testData['desc'] + + test_name = "%s (%s)" % (name, spec_filename) + def test(self): template = testData['template'] - partials = testData.has_key('partials') and test['partials'] or {} + partials = testData.has_key('partials') and testData['partials'] or {} expected = testData['expected'] data = testData['data'] - self.assertEquals(pystache.render(template, data), expected) - test.__doc__ = testData['desc'] - test.__name__ = 'test %s (%s)' % (testData['name'], spec) + renderer = Renderer(loader=partials) + actual = renderer.render(template, data).encode('utf-8') + + message = """%s + + Template: \"""%s\""" + + Expected: %s + Actual: %s""" % (description, template, repr(expected), repr(actual)) + + self.assertEquals(actual, expected, message) + + # The name must begin with "test" for nosetests test discovery to work. + test.__name__ = 'test: "%s"' % test_name + return test for spec in specs: - name = os.path.basename(spec).replace('.yml', '') + file_name = os.path.basename(spec) for test in yaml.load(open(spec))['tests']: - test = buildTest(test, name) + test = buildTest(test, file_name) setattr(MustacheSpec, test.__name__, test) if __name__ == '__main__': -- cgit v1.2.1 From f8a5be7ea859738d736a123ecf9e8b454bf04b09 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 15:22:09 -0800 Subject: Delete the local test variable to prevent a test-run error. Currently, `nosetests --with-doctest` (which includes mustache spec tests) is at the following: ---------------------------------------------------------------------- Ran 234 tests in 0.407s FAILED (errors=3, failures=27) All errors/failures are with spec tests. --- tests/test_spec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_spec.py b/tests/test_spec.py index b51900a..72caac1 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -61,6 +61,8 @@ for spec in specs: for test in yaml.load(open(spec))['tests']: test = buildTest(test, file_name) setattr(MustacheSpec, test.__name__, test) + # Prevent this variable from being interpreted as another test. + del(test) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From c14459c3dbb55ad787b4c74a58281ba61bf64095 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 26 Dec 2011 11:56:48 -0800 Subject: Addressed issue #73: spec tests now excluded by default. --- README.rst | 1 + tests/spec_cases.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_spec.py | 68 ---------------------------------------------- 3 files changed, 78 insertions(+), 68 deletions(-) create mode 100644 tests/spec_cases.py delete mode 100644 tests/test_spec.py diff --git a/README.rst b/README.rst index 4db8a03..769e8a4 100644 --- a/README.rst +++ b/README.rst @@ -71,6 +71,7 @@ To include tests from the mustache spec_ in your test runs: :: git submodule init git submodule update + nosetests -i spec Mailing List diff --git a/tests/spec_cases.py b/tests/spec_cases.py new file mode 100644 index 0000000..f8f6401 --- /dev/null +++ b/tests/spec_cases.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" +Creates a unittest.TestCase for the tests defined in the mustache spec. + +We did not call this file something like "test_spec.py" to avoid matching +nosetests's default regular expression "(?:^|[\b_\./-])[Tt]est". +This allows us to exclude the spec test cases by default when running +nosetests. To include the spec tests, one can use the following option, +for example-- + + nosetests -i spec + +""" + +import glob +import os.path +import sys +import unittest +import yaml + +from pystache.renderer import Renderer + + +def code_constructor(loader, node): + value = loader.construct_mapping(node) + return eval(value['python'], {}) + +yaml.add_constructor(u'!code', code_constructor) + +specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') +specs = glob.glob(os.path.join(specs, '*.yml')) + +class MustacheSpec(unittest.TestCase): + pass + +def buildTest(testData, spec_filename): + + name = testData['name'] + description = testData['desc'] + + test_name = "%s (%s)" % (name, spec_filename) + + def test(self): + template = testData['template'] + partials = testData.has_key('partials') and testData['partials'] or {} + expected = testData['expected'] + data = testData['data'] + + renderer = Renderer(loader=partials) + actual = renderer.render(template, data).encode('utf-8') + + message = """%s + + Template: \"""%s\""" + + Expected: %s + Actual: %s""" % (description, template, repr(expected), repr(actual)) + + self.assertEquals(actual, expected, message) + + # The name must begin with "test" for nosetests test discovery to work. + test.__name__ = 'test: "%s"' % test_name + + return test + +for spec in specs: + file_name = os.path.basename(spec) + + for test in yaml.load(open(spec))['tests']: + test = buildTest(test, file_name) + setattr(MustacheSpec, test.__name__, test) + # Prevent this variable from being interpreted as another test. + del(test) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_spec.py b/tests/test_spec.py deleted file mode 100644 index 72caac1..0000000 --- a/tests/test_spec.py +++ /dev/null @@ -1,68 +0,0 @@ -# coding: utf-8 - -""" -Tests the mustache spec test cases. - -""" - -import glob -import os.path -import unittest -import yaml - -from pystache.renderer import Renderer - - -def code_constructor(loader, node): - value = loader.construct_mapping(node) - return eval(value['python'], {}) - -yaml.add_constructor(u'!code', code_constructor) - -specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') -specs = glob.glob(os.path.join(specs, '*.yml')) - -class MustacheSpec(unittest.TestCase): - pass - -def buildTest(testData, spec_filename): - - name = testData['name'] - description = testData['desc'] - - test_name = "%s (%s)" % (name, spec_filename) - - def test(self): - template = testData['template'] - partials = testData.has_key('partials') and testData['partials'] or {} - expected = testData['expected'] - data = testData['data'] - - renderer = Renderer(loader=partials) - actual = renderer.render(template, data).encode('utf-8') - - message = """%s - - Template: \"""%s\""" - - Expected: %s - Actual: %s""" % (description, template, repr(expected), repr(actual)) - - self.assertEquals(actual, expected, message) - - # The name must begin with "test" for nosetests test discovery to work. - test.__name__ = 'test: "%s"' % test_name - - return test - -for spec in specs: - file_name = os.path.basename(spec) - - for test in yaml.load(open(spec))['tests']: - test = buildTest(test, file_name) - setattr(MustacheSpec, test.__name__, test) - # Prevent this variable from being interpreted as another test. - del(test) - -if __name__ == '__main__': - unittest.main() -- cgit v1.2.1 From 0cd37dcf7198ebce53ca92df92617c6604aa88aa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 26 Dec 2011 12:00:51 -0800 Subject: Removed extra import statement. --- tests/spec_cases.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/spec_cases.py b/tests/spec_cases.py index f8f6401..480aecd 100644 --- a/tests/spec_cases.py +++ b/tests/spec_cases.py @@ -15,7 +15,6 @@ for example-- import glob import os.path -import sys import unittest import yaml -- cgit v1.2.1 From 7a4dbe22107f697d30af48ae3b9d77548802f227 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 26 Dec 2011 12:25:48 -0800 Subject: Added new Reader class and tests. --- pystache/reader.py | 55 +++++++++++++++++++++++++++++++++++ tests/test_reader.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 pystache/reader.py create mode 100644 tests/test_reader.py diff --git a/pystache/reader.py b/pystache/reader.py new file mode 100644 index 0000000..4a2ec10 --- /dev/null +++ b/pystache/reader.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +""" +This module provides a Reader class to read a template given a path. + +""" + +from __future__ import with_statement + +import os +import sys + + +DEFAULT_DECODE_ERRORS = 'strict' + + +class Reader(object): + + def __init__(self, encoding=None, decode_errors=None): + """ + Construct a template reader. + + Arguments: + + encoding: the file encoding. This is the name of the encoding to + use when converting file contents to unicode. This name is + passed as the encoding argument to Python's built-in function + unicode(). Defaults to the encoding name returned by + sys.getdefaultencoding(). + + decode_errors: the string to pass as the errors argument to the + built-in function unicode() when converting file contents to + unicode. Defaults to "strict". + + """ + if decode_errors is None: + decode_errors = DEFAULT_DECODE_ERRORS + + if encoding is None: + encoding = sys.getdefaultencoding() + + self.decode_errors = decode_errors + self.encoding = encoding + + def read(self, path): + """ + Read the template at the given path, and return it as a unicode string. + + """ + with open(path, 'r') as f: + text = f.read() + + text = unicode(text, self.encoding, self.decode_errors) + + return text diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..1a768d4 --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,81 @@ +# encoding: utf-8 + +""" +Unit tests of reader.py. + +""" + +import os +import sys +import unittest + +from pystache.reader import Reader + + +DATA_DIR = 'tests/data' + + +class ReaderTestCase(unittest.TestCase): + + def _get_path(self, filename): + return os.path.join(DATA_DIR, filename) + + def test_init__decode_errors(self): + # Test the default value. + reader = Reader() + self.assertEquals(reader.decode_errors, 'strict') + + reader = Reader(decode_errors='replace') + self.assertEquals(reader.decode_errors, 'replace') + + def test_init__encoding(self): + # Test the default value. + reader = Reader() + self.assertEquals(reader.encoding, sys.getdefaultencoding()) + + reader = Reader(encoding='foo') + self.assertEquals(reader.encoding, 'foo') + + def test_read(self): + """ + Test read(). + + """ + reader = Reader() + path = self._get_path('ascii.mustache') + self.assertEquals(reader.read(path), 'ascii: abc') + + def test_read__returns_unicode(self): + """ + Test that read() returns unicode strings. + + """ + reader = Reader() + path = self._get_path('ascii.mustache') + contents = reader.read(path) + self.assertEqual(type(contents), unicode) + + def test_read__encoding(self): + """ + Test read(): encoding attribute respected. + + """ + reader = Reader() + path = self._get_path('nonascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + reader.encoding = 'utf-8' + self.assertEquals(reader.read(path), u'non-ascii: é') + + def test_get__decode_errors(self): + """ + Test get(): decode_errors attribute. + + """ + reader = Reader() + path = self._get_path('nonascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + reader.decode_errors = 'replace' + self.assertEquals(reader.read(path), u'non-ascii: \ufffd\ufffd') + -- cgit v1.2.1 From 14d9932a6a09cfc6e4151100227142556a49a7af Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 26 Dec 2011 12:56:10 -0800 Subject: Switched the Loader class to using the new Reader class. --- pystache/loader.py | 51 ++++++++++++++++++-------------------------------- pystache/renderer.py | 4 +++- pystache/view.py | 4 +++- tests/test_loader.py | 50 ++++++++++++------------------------------------- tests/test_renderer.py | 30 +++++++++++++---------------- 5 files changed, 49 insertions(+), 90 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 4647bbb..4ffb966 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -8,13 +8,15 @@ This module provides a Loader class. import os import sys -DEFAULT_DECODE_ERRORS = 'strict' +from .reader import Reader + + DEFAULT_EXTENSION = 'mustache' class Loader(object): - def __init__(self, search_dirs=None, extension=None, encoding=None, decode_errors=None): + def __init__(self, search_dirs=None, extension=None, reader=None): """ Construct a template loader. @@ -28,21 +30,13 @@ class Loader(object): extension: the template file extension. Defaults to "mustache". Pass False for no extension (i.e. extensionless template files). - encoding: the name of the encoding to use when converting file - contents to unicode. This name will be passed as the encoding - argument to the built-in function unicode(). Defaults to the - encoding name returned by sys.getdefaultencoding(). - - decode_errors: the string to pass as the "errors" argument to the - built-in function unicode() when converting file contents to - unicode. Defaults to "strict". + reader: the Reader instance to use to read file contents and + return them as unicode strings. Defaults to constructing + the default Reader with no constructor arguments. """ - if decode_errors is None: - decode_errors = DEFAULT_DECODE_ERRORS - - if encoding is None: - encoding = sys.getdefaultencoding() + if reader is None: + reader = Reader() if extension is None: extension = DEFAULT_EXTENSION @@ -53,11 +47,17 @@ class Loader(object): if isinstance(search_dirs, basestring): search_dirs = [search_dirs] - self.decode_errors = decode_errors + self.reader = reader self.search_dirs = search_dirs - self.template_encoding = encoding self.template_extension = extension + def _read(self, path): + """ + Read and return a template as a unicode string. + + """ + return self.reader.read(path) + def make_file_name(self, template_name): file_name = template_name if self.template_extension is not False: @@ -79,23 +79,8 @@ class Loader(object): for dir_path in search_dirs: file_path = os.path.join(dir_path, file_name) if os.path.exists(file_path): - return self._load_template_file(file_path) + return self._read(file_path) # TODO: we should probably raise an exception of our own type. raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) - def _load_template_file(self, file_path): - """ - Read a template file, and return it as a string. - - """ - f = open(file_path, 'r') - - try: - template = f.read() - finally: - f.close() - - template = unicode(template, self.template_encoding, self.decode_errors) - - return template diff --git a/pystache/renderer.py b/pystache/renderer.py index 591e16e..6ad4e39 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -10,6 +10,7 @@ import sys from .context import Context from .loader import Loader +from .reader import Reader from .renderengine import RenderEngine @@ -86,7 +87,8 @@ class Renderer(object): escape = lambda s: cgi.escape(s, quote=True) if loader is None: - loader = Loader(encoding=default_encoding, decode_errors=decode_errors) + reader = Reader(encoding=default_encoding, decode_errors=decode_errors) + loader = Loader(reader=reader) self.decode_errors = decode_errors self.default_encoding = default_encoding diff --git a/pystache/view.py b/pystache/view.py index ca11f7c..9dc5444 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -10,6 +10,7 @@ from types import UnboundMethodType from .context import Context from .loader import Loader +from .reader import Reader from .renderer import Renderer @@ -57,7 +58,8 @@ class View(object): # user did not supply a load_template to the constructor) # to let users set the template_extension attribute, etc. after # View.__init__() has already been called. - loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, + reader = Reader(encoding=self.template_encoding) + loader = Loader(search_dirs=self.template_path, reader=reader, extension=self.template_extension) self._loader = loader diff --git a/tests/test_loader.py b/tests/test_loader.py index 49c4348..9e30acc 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -5,6 +5,7 @@ import sys import unittest from pystache.loader import Loader +from pystache.reader import Reader DATA_DIR = 'tests/data' @@ -23,22 +24,6 @@ class LoaderTestCase(unittest.TestCase): loader = Loader(search_dirs=['foo']) self.assertEquals(loader.search_dirs, ['foo']) - def test_init__decode_errors(self): - # Test the default value. - loader = Loader() - self.assertEquals(loader.decode_errors, 'strict') - - loader = Loader(decode_errors='replace') - self.assertEquals(loader.decode_errors, 'replace') - - def test_init__encoding(self): - # Test the default value. - loader = Loader() - self.assertEquals(loader.template_encoding, sys.getdefaultencoding()) - - loader = Loader(encoding='foo') - self.assertEquals(loader.template_encoding, 'foo') - def test_init__extension(self): # Test the default value. loader = Loader() @@ -50,6 +35,17 @@ class LoaderTestCase(unittest.TestCase): loader = Loader(extension=False) self.assertTrue(loader.template_extension is False) + def test_init__reader(self): + # Test the default value. + loader = Loader() + reader = loader.reader + self.assertEquals(reader.encoding, sys.getdefaultencoding()) + self.assertEquals(reader.decode_errors, 'strict') + + reader = Reader() + loader = Loader(reader=reader) + self.assertTrue(loader.reader is reader) + def test_make_file_name(self): loader = Loader() @@ -103,25 +99,3 @@ class LoaderTestCase(unittest.TestCase): actual = loader.get('ascii') self.assertEqual(type(actual), unicode) - def test_get__encoding(self): - """ - Test get(): encoding attribute respected. - - """ - loader = self._loader() - - self.assertRaises(UnicodeDecodeError, loader.get, 'nonascii') - loader.template_encoding = 'utf-8' - self.assertEquals(loader.get('nonascii'), u'non-ascii: é') - - def test_get__decode_errors(self): - """ - Test get(): decode_errors attribute. - - """ - loader = self._loader() - - self.assertRaises(UnicodeDecodeError, loader.get, 'nonascii') - loader.decode_errors = 'replace' - self.assertEquals(loader.get('nonascii'), u'non-ascii: \ufffd\ufffd') - diff --git a/tests/test_renderer.py b/tests/test_renderer.py index b6b3c65..90647a3 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -35,39 +35,35 @@ class RendererInitTestCase(unittest.TestCase): Test that the default loader is constructed correctly. """ - r = Renderer() - actual = r.loader + renderer = Renderer() + actual = renderer.loader expected = Loader() self.assertEquals(type(actual), type(expected)) - self.assertEquals(actual.__dict__, expected.__dict__) + self.assertEquals(actual.template_extension, expected.template_extension) + self.assertEquals(actual.search_dirs, expected.search_dirs) + self.assertEquals(actual.reader.__dict__, expected.reader.__dict__) def test_loader__default__default_encoding(self): """ - Test that the default loader inherits the default_encoding. + Test that the default loader inherits default_encoding. """ - r = Renderer(default_encoding='foo') - actual = r.loader + renderer = Renderer(default_encoding='foo') + reader = renderer.loader.reader - expected = Loader(encoding='foo') - self.assertEquals(actual.template_encoding, expected.template_encoding) - # Check all attributes for good measure. - self.assertEquals(actual.__dict__, expected.__dict__) + self.assertEquals(reader.encoding, 'foo') def test_loader__default__decode_errors(self): """ - Test that the default loader inherits the decode_errors. + Test that the default loader inherits decode_errors. """ - r = Renderer(decode_errors='foo') - actual = r.loader + renderer = Renderer(decode_errors='foo') + reader = renderer.loader.reader - expected = Loader(decode_errors='foo') - self.assertEquals(actual.decode_errors, expected.decode_errors) - # Check all attributes for good measure. - self.assertEquals(actual.__dict__, expected.__dict__) + self.assertEquals(reader.decode_errors, 'foo') def test_escape__default(self): escape = Renderer().escape -- cgit v1.2.1 From d9769c86c412a55614569e7c6e389f2bb2dd0fcb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 26 Dec 2011 12:58:41 -0800 Subject: Updated the Renderer.__init__() docstring. --- pystache/renderer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 6ad4e39..bd1e6e5 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -48,9 +48,8 @@ class Renderer(object): as a unicode string. If there is no template with that name, the method should either return None (as dict.get() does) or raise an exception. - Defaults to constructing a Loader instance with - default_encoding and decode_errors passed as the encoding and - decode_errors arguments, respectively. + Defaults to constructing a default Loader, but using the + default_encoding and decode_errors arguments. escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode -- cgit v1.2.1 From b09cd3031ddf1c0eb2a1d79952dc50646a0ee929 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 09:01:32 -0800 Subject: Addressed issue #74: "Add Renderer.render_path()" This commit also adds a Renderer.read() method and a file_encoding keyword argument to Renderer.__init__(). --- pystache/renderer.py | 43 ++++++++++++++++++++++++----- tests/common.py | 15 +++++++++++ tests/data/say_hello.mustache | 1 + tests/test_loader.py | 2 +- tests/test_renderer.py | 63 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 tests/common.py create mode 100644 tests/data/say_hello.mustache diff --git a/pystache/renderer.py b/pystache/renderer.py index bd1e6e5..d5532e0 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -33,8 +33,8 @@ class Renderer(object): """ - def __init__(self, loader=None, default_encoding=None, decode_errors='strict', - escape=None): + def __init__(self, loader=None, file_encoding=None, default_encoding=None, + decode_errors='strict', escape=None): """ Construct an instance. @@ -64,21 +64,29 @@ class Renderer(object): escape function, for example. One may also wish to consider using markupsafe's escape function: markupsafe.escape(). + file_encoding: the name of the encoding of all template files. + This encoding is used when reading and converting any template + files to unicode. All templates are converted to unicode prior + to parsing. Defaults to the default_encoding argument. + default_encoding: the name of the encoding to use when converting - to unicode any strings of type `str` encountered during the - rendering process. The name will be passed as the "encoding" + to unicode any strings of type str encountered during the + rendering process. The name will be passed as the encoding argument to the built-in function unicode(). Defaults to the encoding name returned by sys.getdefaultencoding(). - decode_errors: the string to pass as the "errors" argument to the + decode_errors: the string to pass as the errors argument to the built-in function unicode() when converting to unicode any - strings of type `str` encountered during the rendering process. + strings of type str encountered during the rendering process. Defaults to "strict". """ if default_encoding is None: default_encoding = sys.getdefaultencoding() + if file_encoding is None: + file_encoding = default_encoding + if escape is None: # The quote=True argument causes double quotes to be escaped, # but not single quotes: @@ -92,6 +100,7 @@ class Renderer(object): self.decode_errors = decode_errors self.default_encoding = default_encoding self.escape = escape + self.file_encoding = file_encoding self.loader = loader def _to_unicode_soft(self, s): @@ -179,6 +188,28 @@ class Renderer(object): escape=self._escape_to_unicode) return engine + def read(self, path): + """ + Read and return as a unicode string the file contents at path. + + This class uses this method whenever it needs to read a template + file. This method uses the file_encoding and decode_errors + attributes. + + """ + reader = Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) + return reader.read(path) + + def render_path(self, template_path, context=None, **kwargs): + """ + Render the template at the given path using the given context. + + Read the render() docstring for more information. + + """ + template = self.read(template_path) + return self.render(template, context, **kwargs) + def render(self, template, context=None, **kwargs): """ Render the given template using the given context. diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..0fa5e38 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,15 @@ +# coding: utf-8 + +""" +Provides test-related code that can be used by all tests. + +""" + +import os + + +DATA_DIR = 'tests/data' + +def get_data_path(file_name): + return os.path.join(DATA_DIR, file_name) + diff --git a/tests/data/say_hello.mustache b/tests/data/say_hello.mustache new file mode 100644 index 0000000..0824db0 --- /dev/null +++ b/tests/data/say_hello.mustache @@ -0,0 +1 @@ +Hello {{to}} \ No newline at end of file diff --git a/tests/test_loader.py b/tests/test_loader.py index 9e30acc..332e318 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -7,7 +7,7 @@ import unittest from pystache.loader import Loader from pystache.reader import Reader -DATA_DIR = 'tests/data' +from .common import DATA_DIR class LoaderTestCase(unittest.TestCase): diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 90647a3..5cd3d89 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -13,6 +13,7 @@ from pystache import renderer from pystache.renderer import Renderer from pystache.loader import Loader +from .common import get_data_path class RendererInitTestCase(unittest.TestCase): @@ -110,6 +111,22 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer(decode_errors="foo") self.assertEquals(renderer.decode_errors, "foo") + def test_file_encoding__default(self): + """ + Check that file_encoding defaults to default_encoding. + + """ + renderer = Renderer() + self.assertEquals(renderer.file_encoding, renderer.default_encoding) + + def test_file_encoding(self): + """ + Check that the file_encoding attribute is set correctly. + + """ + renderer = Renderer(file_encoding='foo') + self.assertEquals(renderer.file_encoding, 'foo') + class RendererTestCase(unittest.TestCase): @@ -150,6 +167,42 @@ class RendererTestCase(unittest.TestCase): # U+FFFD is the official Unicode replacement character. self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf') + ## Test the read() method. + + def _read(self, renderer, filename): + path = get_data_path(filename) + return renderer.read(path) + + def test_read(self): + renderer = Renderer() + actual = self._read(renderer, 'ascii.mustache') + self.assertEquals(actual, 'ascii: abc') + + def test_read__returns_unicode(self): + renderer = Renderer() + actual = self._read(renderer, 'ascii.mustache') + self.assertEquals(type(actual), unicode) + + def test_read__file_encoding(self): + filename = 'nonascii.mustache' + + renderer = Renderer() + renderer.file_encoding = 'ascii' + + self.assertRaises(UnicodeDecodeError, self._read, renderer, filename) + renderer.file_encoding = 'utf-8' + actual = self._read(renderer, filename) + self.assertEquals(actual, u'non-ascii: é') + + def test_read__decode_errors(self): + filename = 'nonascii.mustache' + renderer = Renderer() + + self.assertRaises(UnicodeDecodeError, self._read, renderer, filename) + renderer.decode_errors = 'ignore' + actual = self._read(renderer, filename) + self.assertEquals(actual, 'non-ascii: ') + ## Test the render() method. def test_render__return_type(self): @@ -267,6 +320,16 @@ class RendererTestCase(unittest.TestCase): # TypeError: decoding Unicode is not supported self.assertEquals(load_partial("partial"), "foo") + def test_render_path(self): + """ + Test the render_path() method. + + """ + renderer = Renderer() + path = get_data_path('say_hello.mustache') + actual = renderer.render_path(path, to='world') + self.assertEquals(actual, "Hello world") + # By testing that Renderer.render() constructs the right RenderEngine, # we no longer need to exercise all rendering code paths through -- cgit v1.2.1 From 4720fa7f65ac6062ec43ec3889fd9fc401a08df1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 18:46:14 -0800 Subject: Starting branch for the View class to load templates using a Renderer. --- pystache/renderer.py | 4 ++-- tests/test_renderer.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index d5532e0..0323800 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -49,7 +49,7 @@ class Renderer(object): the method should either return None (as dict.get() does) or raise an exception. Defaults to constructing a default Loader, but using the - default_encoding and decode_errors arguments. + file_encoding and decode_errors arguments. escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode @@ -94,7 +94,7 @@ class Renderer(object): escape = lambda s: cgi.escape(s, quote=True) if loader is None: - reader = Reader(encoding=default_encoding, decode_errors=decode_errors) + reader = Reader(encoding=file_encoding, decode_errors=decode_errors) loader = Loader(reader=reader) self.decode_errors = decode_errors diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5cd3d89..9ee9b87 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -46,12 +46,12 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(actual.search_dirs, expected.search_dirs) self.assertEquals(actual.reader.__dict__, expected.reader.__dict__) - def test_loader__default__default_encoding(self): + def test_loader__default__encoding(self): """ - Test that the default loader inherits default_encoding. + Test that the default loader inherits the correct encoding. """ - renderer = Renderer(default_encoding='foo') + renderer = Renderer(file_encoding='foo') reader = renderer.loader.reader self.assertEquals(reader.encoding, 'foo') -- cgit v1.2.1 From 98e4f43ff7e5441a113127c6427dbaa75530c54c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 18:46:14 -0800 Subject: The View class now does all its loading through a Renderer instance. This will help with future refactoring and code maintenance by not having the same logic duplicated in more than one part of the code. It also takes more responsibility away from the View class, which is what we want. --- pystache/renderer.py | 39 +++++++++++++++++++++++++++---- pystache/view.py | 44 +++++++++++++++-------------------- tests/test_examples.py | 4 ---- tests/test_renderer.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++- tests/test_view.py | 19 +++------------ 5 files changed, 118 insertions(+), 51 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 0323800..72aed73 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -6,9 +6,11 @@ This module provides a Renderer class to render templates. """ import cgi +import os import sys from .context import Context +from .loader import DEFAULT_EXTENSION from .loader import Loader from .reader import Reader from .renderengine import RenderEngine @@ -34,7 +36,8 @@ class Renderer(object): """ def __init__(self, loader=None, file_encoding=None, default_encoding=None, - decode_errors='strict', escape=None): + decode_errors='strict', search_dirs=None, file_extension=None, + escape=None): """ Construct an instance. @@ -80,28 +83,54 @@ class Renderer(object): strings of type str encountered during the rendering process. Defaults to "strict". + search_dirs: the list of directories in which to search for + templates when loading a template by name. Defaults to the + current working directory. If given a string, the string is + interpreted as a single directory. + + file_extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. for extensionless files). + """ if default_encoding is None: default_encoding = sys.getdefaultencoding() - if file_encoding is None: - file_encoding = default_encoding - if escape is None: # The quote=True argument causes double quotes to be escaped, # but not single quotes: # http://docs.python.org/library/cgi.html#cgi.escape escape = lambda s: cgi.escape(s, quote=True) + # This needs to be after we set the default default_encoding. + if file_encoding is None: + file_encoding = default_encoding + + if file_extension is None: + file_extension = DEFAULT_EXTENSION + + if search_dirs is None: + search_dirs = os.curdir # i.e. "." + + if isinstance(search_dirs, basestring): + search_dirs = [search_dirs] + + # This needs to be after we set some of the defaults above. if loader is None: reader = Reader(encoding=file_encoding, decode_errors=decode_errors) - loader = Loader(reader=reader) + loader = Loader(reader=reader, search_dirs=search_dirs, extension=file_extension) self.decode_errors = decode_errors self.default_encoding = default_encoding self.escape = escape self.file_encoding = file_encoding + self.file_extension = file_extension + # TODO: we should not store a loader attribute because the loader + # would no longer reflect the current attributes if, say, someone + # changed the search_dirs attribute after instantiation. Instead, + # we should construct the Loader instance each time on the fly, + # as we do with the Reader in the read() method. self.loader = loader + self.search_dirs = search_dirs def _to_unicode_soft(self, s): """ diff --git a/pystache/view.py b/pystache/view.py index 9dc5444..bc48f9f 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -23,6 +23,7 @@ class View(object): template_extension = None _loader = None + _renderer = None def __init__(self, template=None, context=None, loader=None, **kwargs): """ @@ -52,22 +53,20 @@ class View(object): self.context = _context - def get_loader(self): - if self._loader is None: - # We delay setting self._loader until now (in the case that the - # user did not supply a load_template to the constructor) - # to let users set the template_extension attribute, etc. after - # View.__init__() has already been called. - reader = Reader(encoding=self.template_encoding) - loader = Loader(search_dirs=self.template_path, reader=reader, - extension=self.template_extension) - self._loader = loader - - return self._loader - - def load_template(self, template_name): - loader = self.get_loader() - return loader.get(template_name) + def _get_renderer(self): + if self._renderer is None: + # We delay setting self._renderer until now (instead of, say, + # setting it in the constructor) in case the user changes after + # instantiation some of the attributes on which the Renderer + # depends. This lets users set the template_extension attribute, + # etc. after View.__init__() has already been called. + renderer = Renderer(loader=self._loader, + file_encoding=self.template_encoding, + search_dirs=self.template_path, + file_extension=self.template_extension) + self._renderer = renderer + + return self._renderer def get_template(self): """ @@ -76,7 +75,8 @@ class View(object): """ if not self.template: template_name = self._get_template_name() - self.template = self.load_template(template_name) + renderer = self._get_renderer() + self.template = renderer.loader.get(template_name) return self.template @@ -102,19 +102,13 @@ class View(object): return re.sub('[A-Z]', repl, template_name)[1:] - # TODO: the View class should probably have some sort of template renderer - # associated with it to encapsulate all of the render-specific behavior - # and options like encoding, escape, etc. This would probably be better - # than passing all of these options to render(), especially as the list - # of possible options grows. - def render(self, escape=None): + def render(self): """ Return the view rendered using the current context. """ - loader = self.get_loader() template = self.get_template() - renderer = Renderer(escape=escape, loader=loader) + renderer = self._get_renderer() return renderer.render(template, self.context) def get(self, key, default=None): diff --git a/tests/test_examples.py b/tests/test_examples.py index 58ea108..d28f8f5 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -31,10 +31,6 @@ class TestView(unittest.TestCase): def test_escaping(self): self.assertEquals(Escaped().render(), "

Bear > Shark

") - def test_escaping__custom(self): - escape = lambda s: s.upper() - self.assertEquals(Escaped().render(escape=escape), "

BEAR > SHARK

") - def test_literal(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 9ee9b87..da5163c 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -6,6 +6,7 @@ Unit tests of template.py. """ import codecs +import os import sys import unittest @@ -66,6 +67,26 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(reader.decode_errors, 'foo') + def test_loader__default__file_extension(self): + """ + Test that the default loader inherits file_extension. + + """ + renderer = Renderer(file_extension='foo') + loader = renderer.loader + + self.assertEquals(loader.template_extension, 'foo') + + def test_loader__default__search_dirs(self): + """ + Test that the default loader inherits search_dirs. + + """ + renderer = Renderer(search_dirs='foo') + loader = renderer.loader + + self.assertEquals(loader.search_dirs, ['foo']) + def test_escape__default(self): escape = Renderer().escape @@ -113,7 +134,7 @@ class RendererInitTestCase(unittest.TestCase): def test_file_encoding__default(self): """ - Check that file_encoding defaults to default_encoding. + Check the file_encoding default. """ renderer = Renderer() @@ -127,6 +148,46 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer(file_encoding='foo') self.assertEquals(renderer.file_encoding, 'foo') + def test_file_extension__default(self): + """ + Check the file_extension default. + + """ + renderer = Renderer() + self.assertEquals(renderer.file_extension, 'mustache') + + def test_file_extension(self): + """ + Check that the file_encoding attribute is set correctly. + + """ + renderer = Renderer(file_extension='foo') + self.assertEquals(renderer.file_extension, 'foo') + + def test_search_dirs__default(self): + """ + Check the search_dirs default. + + """ + renderer = Renderer() + self.assertEquals(renderer.search_dirs, [os.curdir]) + + def test_search_dirs__string(self): + """ + Check that the search_dirs attribute is set correctly when a string. + + """ + renderer = Renderer(search_dirs='foo') + self.assertEquals(renderer.search_dirs, ['foo']) + + def test_search_dirs__list(self): + """ + Check that the search_dirs attribute is set correctly when a list. + + """ + renderer = Renderer(search_dirs=['foo']) + self.assertEquals(renderer.search_dirs, ['foo']) + class RendererTestCase(unittest.TestCase): diff --git a/tests/test_view.py b/tests/test_view.py index bd2b616..e28f1e2 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -46,29 +46,16 @@ class ViewTestCase(unittest.TestCase): view = Simple(thing='world') self.assertEquals(view.render(), "Hi world!") - def test_load_template(self): - """ - Test View.load_template(). - - """ - template = Simple().load_template("escaped") - self.assertEquals(template, "

{{title}}

") - - def test_load_template__extensionless_file(self): - view = Simple() - view.template_extension = False - template = view.load_template('extensionless') - self.assertEquals(template, "No file extension: {{foo}}") - def test_load_template__custom_loader(self): """ Test passing a custom loader to View.__init__(). """ + template = "{{>partial}}" partials = {"partial": "Loaded from dictionary"} - view = Simple(loader=partials) + view = Simple(template=template, loader=partials) + actual = view.render() - actual = view.load_template("partial") self.assertEquals(actual, "Loaded from dictionary") def test_template_path(self): -- cgit v1.2.1 From e256e732d1a5a50c9521f2ebda04315f35741cf1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 21:55:52 -0800 Subject: Added a load_template() method to the Renderer class. --- pystache/renderer.py | 77 +++++++++++++++++++++--------- pystache/view.py | 2 +- tests/test_renderer.py | 124 ++++++++++++++++++++++++++++++------------------- 3 files changed, 133 insertions(+), 70 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 72aed73..727e07d 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -16,6 +16,12 @@ from .reader import Reader from .renderengine import RenderEngine +# The quote=True argument causes double quotes to be escaped, +# but not single quotes: +# http://docs.python.org/library/cgi.html#cgi.escape +DEFAULT_ESCAPE = lambda s: cgi.escape(s, quote=True) + + class Renderer(object): """ @@ -35,6 +41,7 @@ class Renderer(object): """ + # TODO: rename the loader argument to "partials". def __init__(self, loader=None, file_encoding=None, default_encoding=None, decode_errors='strict', search_dirs=None, file_extension=None, escape=None): @@ -43,16 +50,17 @@ class Renderer(object): Arguments: - loader: the object (e.g. pystache.Loader or dictionary) that will - load templates during the rendering process, for example when - loading a partial. + loader: an object (e.g. pystache.Loader or dictionary) for custom + partial loading during the rendering process. The loader should have a get() method that accepts a string and returns the corresponding template as a string, preferably as a unicode string. If there is no template with that name, the method should either return None (as dict.get() does) or raise an exception. - Defaults to constructing a default Loader, but using the - file_encoding and decode_errors arguments. + If this argument is None, partial loading takes place using + the normal procedure of reading templates from the file system + using the Loader-related instance attributes (search_dirs, + file_encoding, etc). escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode @@ -96,10 +104,7 @@ class Renderer(object): default_encoding = sys.getdefaultencoding() if escape is None: - # The quote=True argument causes double quotes to be escaped, - # but not single quotes: - # http://docs.python.org/library/cgi.html#cgi.escape - escape = lambda s: cgi.escape(s, quote=True) + escape = DEFAULT_ESCAPE # This needs to be after we set the default default_encoding. if file_encoding is None: @@ -114,21 +119,12 @@ class Renderer(object): if isinstance(search_dirs, basestring): search_dirs = [search_dirs] - # This needs to be after we set some of the defaults above. - if loader is None: - reader = Reader(encoding=file_encoding, decode_errors=decode_errors) - loader = Loader(reader=reader, search_dirs=search_dirs, extension=file_extension) - self.decode_errors = decode_errors self.default_encoding = default_encoding self.escape = escape self.file_encoding = file_encoding self.file_extension = file_extension - # TODO: we should not store a loader attribute because the loader - # would no longer reflect the current attributes if, say, someone - # changed the search_dirs attribute after instantiation. Instead, - # we should construct the Loader instance each time on the fly, - # as we do with the Reader in the read() method. + # TODO: rename self.loader to self.partials. self.loader = loader self.search_dirs = search_dirs @@ -191,9 +187,39 @@ class Renderer(object): return context + def _make_reader(self): + """ + Create a Reader instance using current attributes. + + """ + return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) + + def _make_loader(self): + """ + Create a Loader instance using current attributes. + + """ + reader = self._make_reader() + loader = Loader(reader=reader, search_dirs=self.search_dirs, extension=self.file_extension) + + return loader + def _make_load_partial(self): + """ + Return the load_partial function to pass to RenderEngine.__init__(). + + """ + if self.loader is None: + loader = self._make_loader() + return loader.get + + # Otherwise, create a load_partial function from the custom loader + # that satisfies RenderEngine requirements (and that provides a + # nicer exception, etc). + loader = self.loader + def load_partial(name): - template = self.loader.get(name) + template = loader.get(name) if template is None: # TODO: make a TemplateNotFoundException type that provides @@ -226,9 +252,18 @@ class Renderer(object): attributes. """ - reader = Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) + reader = self._make_reader() return reader.read(path) + # TODO: add unit tests for this method. + def load_template(self, template_name): + """ + Load a template by name from the file system. + + """ + loader = self._make_loader() + return loader.get(template_name) + def render_path(self, template_path, context=None, **kwargs): """ Render the template at the given path using the given context. diff --git a/pystache/view.py b/pystache/view.py index bc48f9f..96aa52c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -76,7 +76,7 @@ class View(object): if not self.template: template_name = self._get_template_name() renderer = self._get_renderer() - self.template = renderer.loader.get(template_name) + self.template = renderer.load_template(template_name) return self.template diff --git a/tests/test_renderer.py b/tests/test_renderer.py index da5163c..7019d36 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -29,8 +29,8 @@ class RendererInitTestCase(unittest.TestCase): """ loader = {'foo': 'bar'} - r = Renderer(loader=loader) - self.assertEquals(r.loader, {'foo': 'bar'}) + renderer = Renderer(loader=loader) + self.assertEquals(renderer.loader, {'foo': 'bar'}) def test_loader__default(self): """ @@ -40,52 +40,7 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer() actual = renderer.loader - expected = Loader() - - self.assertEquals(type(actual), type(expected)) - self.assertEquals(actual.template_extension, expected.template_extension) - self.assertEquals(actual.search_dirs, expected.search_dirs) - self.assertEquals(actual.reader.__dict__, expected.reader.__dict__) - - def test_loader__default__encoding(self): - """ - Test that the default loader inherits the correct encoding. - - """ - renderer = Renderer(file_encoding='foo') - reader = renderer.loader.reader - - self.assertEquals(reader.encoding, 'foo') - - def test_loader__default__decode_errors(self): - """ - Test that the default loader inherits decode_errors. - - """ - renderer = Renderer(decode_errors='foo') - reader = renderer.loader.reader - - self.assertEquals(reader.decode_errors, 'foo') - - def test_loader__default__file_extension(self): - """ - Test that the default loader inherits file_extension. - - """ - renderer = Renderer(file_extension='foo') - loader = renderer.loader - - self.assertEquals(loader.template_extension, 'foo') - - def test_loader__default__search_dirs(self): - """ - Test that the default loader inherits search_dirs. - - """ - renderer = Renderer(search_dirs='foo') - loader = renderer.loader - - self.assertEquals(loader.search_dirs, ['foo']) + self.assertTrue(renderer.loader is None) def test_escape__default(self): escape = Renderer().escape @@ -264,6 +219,79 @@ class RendererTestCase(unittest.TestCase): actual = self._read(renderer, filename) self.assertEquals(actual, 'non-ascii: ') + ## Test the _make_loader() method. + + def test__make_loader__return_type(self): + """ + Test that _make_loader() returns a Loader. + + """ + renderer = Renderer() + loader = renderer._make_loader() + + self.assertEquals(type(loader), Loader) + + def test__make_loader__file_encoding(self): + """ + Test that _make_loader() respects the file_encoding attribute. + + """ + renderer = Renderer() + renderer.file_encoding = 'foo' + + loader = renderer._make_loader() + + self.assertEquals(loader.reader.encoding, 'foo') + + def test__make_loader__decode_errors(self): + """ + Test that _make_loader() respects the decode_errors attribute. + + """ + renderer = Renderer() + renderer.decode_errors = 'foo' + + loader = renderer._make_loader() + + self.assertEquals(loader.reader.decode_errors, 'foo') + + def test__make_loader__file_extension(self): + """ + Test that _make_loader() respects the file_extension attribute. + + """ + renderer = Renderer() + renderer.file_extension = 'foo' + + loader = renderer._make_loader() + + self.assertEquals(loader.template_extension, 'foo') + + def test__make_loader__search_dirs(self): + """ + Test that _make_loader() respects the search_dirs attribute. + + """ + renderer = Renderer() + renderer.search_dirs = ['foo'] + + loader = renderer._make_loader() + + self.assertEquals(loader.search_dirs, ['foo']) + + # This test is a sanity check. Strictly speaking, it shouldn't + # be necessary based on our tests above. + def test__make_loader__default(self): + renderer = Renderer() + actual = renderer._make_loader() + + expected = Loader() + + self.assertEquals(type(actual), type(expected)) + self.assertEquals(actual.template_extension, expected.template_extension) + self.assertEquals(actual.search_dirs, expected.search_dirs) + self.assertEquals(actual.reader.__dict__, expected.reader.__dict__) + ## Test the render() method. def test_render__return_type(self): -- cgit v1.2.1 From aaed5225e4dcc775fcda193fdd58c95859dc7f79 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 22:08:40 -0800 Subject: Renamed the loader argument of Renderer.__init__() to "partials". --- pystache/renderer.py | 40 +++++++++++++++++++--------------------- pystache/view.py | 2 +- tests/test_renderer.py | 31 ++++++++++++++----------------- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 727e07d..8de8d8b 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -29,38 +29,37 @@ class Renderer(object): This class supports several rendering options which are described in the constructor's docstring. Among these, the constructor supports - passing a custom template loader. + passing a custom partial loader. - Here is an example of passing a custom template loader to render a - template using partials loaded from a string-string dictionary. + Here is an example of rendering a template using a custom partial loader + that loads partials loaded from a string-string dictionary. >>> partials = {'partial': 'Hello, {{thing}}!'} - >>> renderer = Renderer(loader=partials) + >>> renderer = Renderer(partials=partials) >>> renderer.render('{{>partial}}', {'thing': 'world'}) u'Hello, world!' """ - # TODO: rename the loader argument to "partials". - def __init__(self, loader=None, file_encoding=None, default_encoding=None, + def __init__(self, file_encoding=None, default_encoding=None, decode_errors='strict', search_dirs=None, file_extension=None, - escape=None): + escape=None, partials=None): """ Construct an instance. Arguments: - loader: an object (e.g. pystache.Loader or dictionary) for custom - partial loading during the rendering process. - The loader should have a get() method that accepts a string + partials: an object (e.g. pystache.Loader or dictionary) for + custom partial loading during the rendering process. + The object should have a get() method that accepts a string and returns the corresponding template as a string, preferably as a unicode string. If there is no template with that name, - the method should either return None (as dict.get() does) or - raise an exception. - If this argument is None, partial loading takes place using - the normal procedure of reading templates from the file system - using the Loader-related instance attributes (search_dirs, - file_encoding, etc). + the get() method should either return None (as dict.get() does) + or raise an exception. + If this argument is None, the rendering process will use + the normal procedure of locating and reading templates from + the file system -- using the Loader-related instance attributes + like search_dirs, file_encoding, etc. escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode @@ -124,8 +123,7 @@ class Renderer(object): self.escape = escape self.file_encoding = file_encoding self.file_extension = file_extension - # TODO: rename self.loader to self.partials. - self.loader = loader + self.partials = partials self.search_dirs = search_dirs def _to_unicode_soft(self, s): @@ -209,17 +207,17 @@ class Renderer(object): Return the load_partial function to pass to RenderEngine.__init__(). """ - if self.loader is None: + if self.partials is None: loader = self._make_loader() return loader.get # Otherwise, create a load_partial function from the custom loader # that satisfies RenderEngine requirements (and that provides a # nicer exception, etc). - loader = self.loader + get_partial = self.partials.get def load_partial(name): - template = loader.get(name) + template = get_partial(name) if template is None: # TODO: make a TemplateNotFoundException type that provides diff --git a/pystache/view.py b/pystache/view.py index 96aa52c..4900777 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -60,7 +60,7 @@ class View(object): # instantiation some of the attributes on which the Renderer # depends. This lets users set the template_extension attribute, # etc. after View.__init__() has already been called. - renderer = Renderer(loader=self._loader, + renderer = Renderer(partials=self._loader, file_encoding=self.template_encoding, search_dirs=self.template_path, file_extension=self.template_extension) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 7019d36..a9fea5b 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -23,24 +23,21 @@ class RendererInitTestCase(unittest.TestCase): """ - def test_loader(self): + def test_partials__default(self): """ - Test that the loader attribute is set correctly. + Test that the default loader is constructed correctly. """ - loader = {'foo': 'bar'} - renderer = Renderer(loader=loader) - self.assertEquals(renderer.loader, {'foo': 'bar'}) + renderer = Renderer() + self.assertTrue(renderer.partials is None) - def test_loader__default(self): + def test_partials(self): """ - Test that the default loader is constructed correctly. + Test that the loader attribute is set correctly. """ - renderer = Renderer() - actual = renderer.loader - - self.assertTrue(renderer.loader is None) + renderer = Renderer(partials={'foo': 'bar'}) + self.assertEquals(renderer.partials, {'foo': 'bar'}) def test_escape__default(self): escape = Renderer().escape @@ -382,8 +379,8 @@ class RendererTestCase(unittest.TestCase): Test the _make_load_partial() method. """ - partials = {'foo': 'bar'} - renderer = Renderer(loader=partials) + renderer = Renderer() + renderer.partials = {'foo': 'bar'} load_partial = renderer._make_load_partial() actual = load_partial('foo') @@ -398,12 +395,12 @@ class RendererTestCase(unittest.TestCase): """ renderer = Renderer() - renderer.loader = {'partial': 'foo'} + renderer.partials = {'partial': 'foo'} load_partial = renderer._make_load_partial() self.assertEquals(load_partial("partial"), "foo") # Now with a value that is already unicode. - renderer.loader = {'partial': u'foo'} + renderer.partials = {'partial': u'foo'} load_partial = renderer._make_load_partial() # If the next line failed, we would get the following error: # TypeError: decoding Unicode is not supported @@ -443,7 +440,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): renderer = Renderer() renderer.default_encoding = 'ascii' - renderer.loader = {'str': 'foo', 'subclass': MyUnicode('abc')} + renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')} engine = renderer._make_render_engine() @@ -462,7 +459,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): """ renderer = Renderer() - renderer.loader = {} + renderer.partials = {} engine = renderer._make_render_engine() load_partial = engine.load_partial -- cgit v1.2.1 From 9d2da6ae6cbb25b75a4242cb2e294c646e41d77d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 22:11:41 -0800 Subject: Renamed the loader argument of View.__init__() to "partials". --- pystache/view.py | 21 ++++++++++----------- tests/test_view.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 4900777..e6ee044 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -25,23 +25,20 @@ class View(object): _loader = None _renderer = None - def __init__(self, template=None, context=None, loader=None, **kwargs): + def __init__(self, template=None, context=None, partials=None, **kwargs): """ Construct a View instance. Arguments: - loader: the object (e.g. pystache.Loader or dictionary) responsible - for loading templates during the rendering process, for example - when loading partials. The object should have a get() method - that accepts a string and returns the corresponding template - as a string, preferably as a unicode string. The method should - return None if there is no template with that name. + partials: the object (e.g. pystache.Loader or dictionary) + responsible for loading partials during the rendering process. + The object should have a get() method that accepts a string and + returns the corresponding template as a string, preferably as a + unicode string. The method should return None if there is no + template with that name, or raise an exception. """ - if loader is not None: - self._loader = loader - if template is not None: self.template = template @@ -51,6 +48,8 @@ class View(object): if kwargs: _context.push(kwargs) + self._partials = partials + self.context = _context def _get_renderer(self): @@ -60,7 +59,7 @@ class View(object): # instantiation some of the attributes on which the Renderer # depends. This lets users set the template_extension attribute, # etc. after View.__init__() has already been called. - renderer = Renderer(partials=self._loader, + renderer = Renderer(partials=self._partials, file_encoding=self.template_encoding, search_dirs=self.template_path, file_extension=self.template_extension) diff --git a/tests/test_view.py b/tests/test_view.py index e28f1e2..3f828b2 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -53,7 +53,7 @@ class ViewTestCase(unittest.TestCase): """ template = "{{>partial}}" partials = {"partial": "Loaded from dictionary"} - view = Simple(template=template, loader=partials) + view = Simple(template=template, partials=partials) actual = view.render() self.assertEquals(actual, "Loaded from dictionary") -- cgit v1.2.1 From 4b884554eeb3897a1c6506f16e52d09b66b5076e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Dec 2011 22:35:49 -0800 Subject: Moved the function to create a template name from View to loader.py. This further trims down the View class. --- pystache/loader.py | 24 ++++++++++++++++++++++++ pystache/view.py | 8 ++------ tests/test_loader.py | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 4ffb966..94dae35 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -6,6 +6,7 @@ This module provides a Loader class. """ import os +import re import sys from .reader import Reader @@ -14,6 +15,29 @@ from .reader import Reader DEFAULT_EXTENSION = 'mustache' +def make_template_name(obj): + """ + Return the canonical template name for an object instance. + + This method converts Python-style class names (PEP 8's recommended + CamelCase, aka CapWords) to lower_case_with_underscords. Here + is an example with code: + + >>> class HelloWorld(object): + ... pass + >>> hi = HelloWorld() + >>> make_template_name(hi) + 'hello_world' + + """ + template_name = obj.__class__.__name__ + + def repl(match): + return '_' + match.group(0).lower() + + return re.sub('[A-Z]', repl, template_name)[1:] + + class Loader(object): def __init__(self, search_dirs=None, extension=None, reader=None): diff --git a/pystache/view.py b/pystache/view.py index e6ee044..f5b0005 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -9,6 +9,7 @@ import re from types import UnboundMethodType from .context import Context +from .loader import make_template_name from .loader import Loader from .reader import Reader from .renderer import Renderer @@ -94,12 +95,7 @@ class View(object): if self.template_name: return self.template_name - template_name = self.__class__.__name__ - - def repl(match): - return '_' + match.group(0).lower() - - return re.sub('[A-Z]', repl, template_name)[1:] + return make_template_name(self) def render(self): """ diff --git a/tests/test_loader.py b/tests/test_loader.py index 332e318..f08eaaa 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -4,11 +4,27 @@ import os import sys import unittest +from pystache.loader import make_template_name from pystache.loader import Loader from pystache.reader import Reader from .common import DATA_DIR + +class MakeTemplateNameTests(unittest.TestCase): + + """ + Test the make_template_name() function. + + """ + + def test(self): + class FooBar(object): + pass + foo = FooBar() + self.assertEquals(make_template_name(foo), 'foo_bar') + + class LoaderTestCase(unittest.TestCase): search_dirs = 'examples' -- cgit v1.2.1 From 7492f81a6bac8052b467e6b0d3a71168cd277e18 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 00:15:20 -0800 Subject: Refactored Renderer._make_context() to use a Context.create() method. --- pystache/context.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ pystache/renderer.py | 39 ++++++++++-------------------- tests/test_context.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 27 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index d585f3f..a3f9ff0 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -67,6 +67,11 @@ class Context(object): Querying the stack for the value of a key queries the items in the stack in order from last-added objects to first (last in, first out). + *Caution*: + + This class currently does not support recursive nesting in that + items in the stack cannot themselves be Context instances. + See the docstrings of the methods of this class for more information. """ @@ -99,9 +104,70 @@ class Context(object): In particular, an item can be an ordinary object with no mapping-like characteristics. + *Caution*: + + Items should not themselves be Context instances, as recursive + nesting does not behave as one might expect. + """ self._stack = list(items) + @staticmethod + def create(*context, **kwargs): + """ + Build a Context instance from a sequence of "mapping-like" objects. + + This factory-style method is more general than the Context class's + constructor in that Context instances can themselves appear in the + argument list. This is not true of the constructor. + + Here is an example illustrating various aspects of this method: + + >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'} + >>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'}) + >>> + >>> context = Context.create(obj1, None, obj2, mineral='gold') + >>> + >>> context.get('animal') + 'cat' + >>> context.get('vegetable') + 'spinach' + >>> context.get('mineral') + 'gold' + + Arguments: + + *context: zero or more dictionaries, Context instances, or objects + with which to populate the initial context stack. None + arguments will be skipped. Items in the *context list are + added to the stack in order so that later items in the argument + list take precedence over earlier items. This behavior is the + same as the constructor's. + + **kwargs: additional key-value data to add to the context stack. + As these arguments appear after all items in the *context list, + in the case of key conflicts these values take precedence over + all items in the *context list. This behavior is the same as + the constructor's. + + """ + items = context + + context = Context() + + for item in items: + if item is None: + continue + if isinstance(item, Context): + context._stack.extend(item._stack) + else: + context.push(item) + + if kwargs: + context.push(kwargs) + + return context + def get(self, key, default=None): """ Query the stack for the given key, and return the resulting value. diff --git a/pystache/renderer.py b/pystache/renderer.py index 8de8d8b..c4ebdfb 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -167,24 +167,6 @@ class Renderer(object): # the default_encoding and decode_errors attributes. return unicode(s, self.default_encoding, self.decode_errors) - def _make_context(self, context, **kwargs): - """ - Initialize the context attribute. - - """ - if context is None: - context = {} - - if isinstance(context, Context): - context = context.copy() - else: - context = Context(context) - - if kwargs: - context.push(kwargs) - - return context - def _make_reader(self): """ Create a Reader instance using current attributes. @@ -262,7 +244,7 @@ class Renderer(object): loader = self._make_loader() return loader.get(template_name) - def render_path(self, template_path, context=None, **kwargs): + def render_path(self, template_path, *context, **kwargs): """ Render the template at the given path using the given context. @@ -270,9 +252,9 @@ class Renderer(object): """ template = self.read(template_path) - return self.render(template, context, **kwargs) + return self.render(template, *context, **kwargs) - def render(self, template, context=None, **kwargs): + def render(self, template, *context, **kwargs): """ Render the given template using the given context. @@ -285,15 +267,20 @@ class Renderer(object): using this instance's default_encoding and decode_errors attributes. See the constructor docstring for more information. - context: a dictionary, Context, or object (e.g. a View instance). + *context: zero or more dictionaries, Context instances, or objects + with which to populate the initial context stack. None + arguments are skipped. Items in the *context list are added to + the context stack in order so that later items in the argument + list take precedence over earlier items. - **kwargs: additional key values to add to the context when - rendering. These values take precedence over the context on - any key conflicts. + **kwargs: additional key-value data to add to the context stack. + As these arguments appear after all items in the *context list, + in the case of key conflicts these values take precedence over + all items in the *context list. """ engine = self._make_render_engine() - context = self._make_context(context, **kwargs) + context = Context.create(*context, **kwargs) # RenderEngine.render() requires that the template string be unicode. template = self._to_unicode_hard(template) diff --git a/tests/test_context.py b/tests/test_context.py index 12bbff6..bf7517c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -168,7 +168,7 @@ class GetItemTestCase(TestCase): self.assertRaises(AttributeError, _get_item, obj, "foo") -class ContextTestCase(TestCase): +class ContextTests(TestCase): """ Test the Context class. @@ -189,6 +189,67 @@ class ContextTestCase(TestCase): """ context = Context({}, {}, {}) + ## Test the static create() method. + + def test_create__dictionary(self): + """ + Test passing a dictionary. + + """ + context = Context.create({'foo': 'bar'}) + self.assertEquals(context.get('foo'), 'bar') + + def test_create__none(self): + """ + Test passing None. + + """ + context = Context.create({'foo': 'bar'}, None) + self.assertEquals(context.get('foo'), 'bar') + + def test_create__object(self): + """ + Test passing an object. + + """ + class Foo(object): + foo = 'bar' + context = Context.create(Foo()) + self.assertEquals(context.get('foo'), 'bar') + + def test_create__context(self): + """ + Test passing a Context instance. + + """ + obj = Context({'foo': 'bar'}) + context = Context.create(obj) + self.assertEquals(context.get('foo'), 'bar') + + def test_create__kwarg(self): + """ + Test passing a keyword argument. + + """ + context = Context.create(foo='bar') + self.assertEquals(context.get('foo'), 'bar') + + def test_create__precedence_positional(self): + """ + Test precedence of positional arguments. + + """ + context = Context.create({'foo': 'bar'}, {'foo': 'buzz'}) + self.assertEquals(context.get('foo'), 'buzz') + + def test_create__precedence_keyword(self): + """ + Test precedence of keyword arguments. + + """ + context = Context.create({'foo': 'bar'}, foo='buzz') + self.assertEquals(context.get('foo'), 'buzz') + def test_get__key_present(self): """ Test getting a key. -- cgit v1.2.1 From d732a8f43fcbc63b42ff62bf85a17fd4a4ed388e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 00:17:42 -0800 Subject: Changed the View class to use the new Context.create(). --- pystache/view.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index f5b0005..980e09b 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -43,15 +43,11 @@ class View(object): if template is not None: self.template = template - _context = Context(self) - if context: - _context.push(context) - if kwargs: - _context.push(kwargs) + context = Context.create(self, context, **kwargs) self._partials = partials - self.context = _context + self.context = context def _get_renderer(self): if self._renderer is None: -- cgit v1.2.1 From ae4a50b9f36599055adf3ec5ac7465aaadd1e399 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:32:09 -0800 Subject: Removed Reader and Loader imports from view.py. --- pystache/view.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 980e09b..fff42c2 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -10,8 +10,6 @@ from types import UnboundMethodType from .context import Context from .loader import make_template_name -from .loader import Loader -from .reader import Reader from .renderer import Renderer -- cgit v1.2.1 From c8ee84f90c4db2c8f7a35a9c86c55f03a5cd8254 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:32:09 -0800 Subject: Removed re and types.UnboundMethodType imports from view.py. --- pystache/view.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index fff42c2..20153e6 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -5,9 +5,6 @@ This module provides a View class. """ -import re -from types import UnboundMethodType - from .context import Context from .loader import make_template_name from .renderer import Renderer -- cgit v1.2.1 From 761ccf447502460f91c9653a39437c52be8b4995 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:32:09 -0800 Subject: Changed the Loader class to a Locator class. Now the class no longer has an indirect dependency on the Reader class. --- pystache/commands.py | 7 ++- pystache/init.py | 3 +- pystache/loader.py | 31 +++----------- pystache/renderer.py | 51 +++++++++++++--------- pystache/view.py | 1 - tests/test_loader.py | 114 +++++++++++++++++++++++-------------------------- tests/test_renderer.py | 63 +++++++++------------------ tests/test_view.py | 6 +-- 8 files changed, 117 insertions(+), 159 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index dc17b88..d6fb549 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -19,7 +19,6 @@ import sys # # ValueError: Attempted relative import in non-package # -from pystache.loader import Loader from pystache.renderer import Renderer @@ -54,8 +53,10 @@ def main(sys_argv): if template.endswith('.mustache'): template = template[:-9] + renderer = Renderer() + try: - template = Loader().get(template) + template = renderer.load_template(template) except IOError: pass @@ -64,8 +65,6 @@ def main(sys_argv): except IOError: context = json.loads(context) - renderer = Renderer() - rendered = renderer.render(template, context) print rendered diff --git a/pystache/init.py b/pystache/init.py index 7d5d4d7..1a60028 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -7,10 +7,9 @@ This module contains the initialization logic called by __init__.py. from .renderer import Renderer from .view import View -from .loader import Loader -__all__ = ['render', 'Loader', 'Renderer', 'View'] +__all__ = ['render', 'Renderer', 'View'] def render(template, context=None, **kwargs): diff --git a/pystache/loader.py b/pystache/loader.py index 94dae35..ee041df 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module provides a Loader class. +This module provides a Locator class. """ @@ -9,8 +9,6 @@ import os import re import sys -from .reader import Reader - DEFAULT_EXTENSION = 'mustache' @@ -38,11 +36,11 @@ def make_template_name(obj): return re.sub('[A-Z]', repl, template_name)[1:] -class Loader(object): +class Locator(object): - def __init__(self, search_dirs=None, extension=None, reader=None): + def __init__(self, search_dirs=None, extension=None): """ - Construct a template loader. + Construct a template locator. Arguments: @@ -54,14 +52,7 @@ class Loader(object): extension: the template file extension. Defaults to "mustache". Pass False for no extension (i.e. extensionless template files). - reader: the Reader instance to use to read file contents and - return them as unicode strings. Defaults to constructing - the default Reader with no constructor arguments. - """ - if reader is None: - reader = Reader() - if extension is None: extension = DEFAULT_EXTENSION @@ -71,17 +62,9 @@ class Loader(object): if isinstance(search_dirs, basestring): search_dirs = [search_dirs] - self.reader = reader self.search_dirs = search_dirs self.template_extension = extension - def _read(self, path): - """ - Read and return a template as a unicode string. - - """ - return self.reader.read(path) - def make_file_name(self, template_name): file_name = template_name if self.template_extension is not False: @@ -89,9 +72,9 @@ class Loader(object): return file_name - def get(self, template_name): + def locate_path(self, template_name): """ - Find and load the given template, and return it as a string. + Find and return the path to the template with the given name. Raises an IOError if the template cannot be found. @@ -103,7 +86,7 @@ class Loader(object): for dir_path in search_dirs: file_path = os.path.join(dir_path, file_name) if os.path.exists(file_path): - return self._read(file_path) + return file_path # TODO: we should probably raise an exception of our own type. raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) diff --git a/pystache/renderer.py b/pystache/renderer.py index c4ebdfb..c286d5f 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -11,7 +11,7 @@ import sys from .context import Context from .loader import DEFAULT_EXTENSION -from .loader import Loader +from .loader import Locator from .reader import Reader from .renderengine import RenderEngine @@ -32,7 +32,7 @@ class Renderer(object): passing a custom partial loader. Here is an example of rendering a template using a custom partial loader - that loads partials loaded from a string-string dictionary. + that loads partials from a string-string dictionary. >>> partials = {'partial': 'Hello, {{thing}}!'} >>> renderer = Renderer(partials=partials) @@ -49,8 +49,8 @@ class Renderer(object): Arguments: - partials: an object (e.g. pystache.Loader or dictionary) for - custom partial loading during the rendering process. + partials: an object (e.g. a dictionary) for custom partial loading + during the rendering process. The object should have a get() method that accepts a string and returns the corresponding template as a string, preferably as a unicode string. If there is no template with that name, @@ -58,8 +58,8 @@ class Renderer(object): or raise an exception. If this argument is None, the rendering process will use the normal procedure of locating and reading templates from - the file system -- using the Loader-related instance attributes - like search_dirs, file_encoding, etc. + the file system -- using relevant instance attributes like + search_dirs, file_encoding, etc. escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode @@ -174,15 +174,26 @@ class Renderer(object): """ return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) - def _make_loader(self): + def _make_locator(self): """ - Create a Loader instance using current attributes. + Create a Locator instance using current attributes. + + """ + return Locator(search_dirs=self.search_dirs, extension=self.file_extension) + + def _make_load_template(self): + """ + Return a function that loads a template by name. """ reader = self._make_reader() - loader = Loader(reader=reader, search_dirs=self.search_dirs, extension=self.file_extension) + locator = self._make_locator() + + def load_template(template_name): + path = locator.locate_path(template_name) + return reader.read(path) - return loader + return load_template def _make_load_partial(self): """ @@ -190,20 +201,20 @@ class Renderer(object): """ if self.partials is None: - loader = self._make_loader() - return loader.get + load_template = self._make_load_template() + return load_template - # Otherwise, create a load_partial function from the custom loader - # that satisfies RenderEngine requirements (and that provides a - # nicer exception, etc). - get_partial = self.partials.get + # Otherwise, create a load_partial function from the custom partial + # loader that satisfies RenderEngine requirements (and that provides + # a nicer exception, etc). + partials = self.partials def load_partial(name): - template = get_partial(name) + template = partials.get(name) if template is None: # TODO: make a TemplateNotFoundException type that provides - # the original loader as an attribute. + # the original partials as an attribute. raise Exception("Partial not found with name: %s" % repr(name)) # RenderEngine requires that the return value be unicode. @@ -241,8 +252,8 @@ class Renderer(object): Load a template by name from the file system. """ - loader = self._make_loader() - return loader.get(template_name) + load_template = self._make_load_template() + return load_template(template_name) def render_path(self, template_path, *context, **kwargs): """ diff --git a/pystache/view.py b/pystache/view.py index 20153e6..4507404 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -18,7 +18,6 @@ class View(object): template_encoding = None template_extension = None - _loader = None _renderer = None def __init__(self, template=None, context=None, partials=None, **kwargs): diff --git a/tests/test_loader.py b/tests/test_loader.py index f08eaaa..7dc726e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,11 +1,16 @@ # encoding: utf-8 +""" +Contains loader.py unit tests. + +""" + import os import sys import unittest from pystache.loader import make_template_name -from pystache.loader import Loader +from pystache.loader import Locator from pystache.reader import Reader from .common import DATA_DIR @@ -25,93 +30,80 @@ class MakeTemplateNameTests(unittest.TestCase): self.assertEquals(make_template_name(foo), 'foo_bar') -class LoaderTestCase(unittest.TestCase): +class LocatorTests(unittest.TestCase): search_dirs = 'examples' - def _loader(self): - return Loader(search_dirs=DATA_DIR) + def _locator(self): + return Locator(search_dirs=DATA_DIR) def test_init__search_dirs(self): # Test the default value. - loader = Loader() - self.assertEquals(loader.search_dirs, [os.curdir]) + locator = Locator() + self.assertEquals(locator.search_dirs, [os.curdir]) - loader = Loader(search_dirs=['foo']) - self.assertEquals(loader.search_dirs, ['foo']) + locator = Locator(search_dirs=['foo']) + self.assertEquals(locator.search_dirs, ['foo']) def test_init__extension(self): # Test the default value. - loader = Loader() - self.assertEquals(loader.template_extension, 'mustache') - - loader = Loader(extension='txt') - self.assertEquals(loader.template_extension, 'txt') + locator = Locator() + self.assertEquals(locator.template_extension, 'mustache') - loader = Loader(extension=False) - self.assertTrue(loader.template_extension is False) + locator = Locator(extension='txt') + self.assertEquals(locator.template_extension, 'txt') - def test_init__reader(self): - # Test the default value. - loader = Loader() - reader = loader.reader - self.assertEquals(reader.encoding, sys.getdefaultencoding()) - self.assertEquals(reader.decode_errors, 'strict') - - reader = Reader() - loader = Loader(reader=reader) - self.assertTrue(loader.reader is reader) + locator = Locator(extension=False) + self.assertTrue(locator.template_extension is False) def test_make_file_name(self): - loader = Loader() + locator = Locator() - loader.template_extension = 'bar' - self.assertEquals(loader.make_file_name('foo'), 'foo.bar') + locator.template_extension = 'bar' + self.assertEquals(locator.make_file_name('foo'), 'foo.bar') - loader.template_extension = False - self.assertEquals(loader.make_file_name('foo'), 'foo') + locator.template_extension = False + self.assertEquals(locator.make_file_name('foo'), 'foo') - loader.template_extension = '' - self.assertEquals(loader.make_file_name('foo'), 'foo.') + locator.template_extension = '' + self.assertEquals(locator.make_file_name('foo'), 'foo.') - def test_get__template_is_loaded(self): - loader = Loader(search_dirs='examples') - template = loader.get('simple') + def test_locate_path(self): + locator = Locator(search_dirs='examples') + path = locator.locate_path('simple') - self.assertEqual(template, 'Hi {{thing}}!{{blank}}') + self.assertEquals(os.path.basename(path), 'simple.mustache') - def test_get__using_list_of_paths(self): - loader = Loader(search_dirs=['doesnt_exist', 'examples']) - template = loader.get('simple') + def test_locate_path__using_list_of_paths(self): + locator = Locator(search_dirs=['doesnt_exist', 'examples']) + path = locator.locate_path('simple') - self.assertEqual(template, 'Hi {{thing}}!{{blank}}') + self.assertTrue(path) - def test_get__non_existent_template_fails(self): - loader = Loader() + def test_locate_path__precedence(self): + """ + Test the order in which locate_path() searches directories. - self.assertRaises(IOError, loader.get, 'doesnt_exist') + """ + locator = Locator() - def test_get__extensionless_file(self): - loader = Loader(search_dirs=self.search_dirs) - self.assertRaises(IOError, loader.get, 'extensionless') + dir1 = DATA_DIR + dir2 = os.path.join(DATA_DIR, 'locator') - loader.template_extension = False - self.assertEquals(loader.get('extensionless'), "No file extension: {{foo}}") + locator.search_dirs = [dir1] + self.assertTrue(locator.locate_path('duplicate')) + locator.search_dirs = [dir2] + self.assertTrue(locator.locate_path('duplicate')) - def test_get(self): - """ - Test get(). + locator.search_dirs = [dir2, dir1] + path = locator.locate_path('duplicate') + dirpath = os.path.dirname(path) + dirname = os.path.split(dirpath)[-1] - """ - loader = self._loader() - self.assertEquals(loader.get('ascii'), 'ascii: abc') + self.assertEquals(dirname, 'locator') - def test_get__unicode_return_value(self): - """ - Test that get() returns unicode strings. + def test_locate_path__non_existent_template_fails(self): + locator = Locator() - """ - loader = self._loader() - actual = loader.get('ascii') - self.assertEqual(type(actual), unicode) + self.assertRaises(IOError, locator.locate_path, 'doesnt_exist') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index a9fea5b..3fb5b59 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -12,7 +12,7 @@ import unittest from pystache import renderer from pystache.renderer import Renderer -from pystache.loader import Loader +from pystache.loader import Locator from .common import get_data_path @@ -25,7 +25,7 @@ class RendererInitTestCase(unittest.TestCase): def test_partials__default(self): """ - Test that the default loader is constructed correctly. + Test the default value. """ renderer = Renderer() @@ -33,7 +33,7 @@ class RendererInitTestCase(unittest.TestCase): def test_partials(self): """ - Test that the loader attribute is set correctly. + Test that the attribute is set correctly. """ renderer = Renderer(partials={'foo': 'bar'}) @@ -216,78 +216,53 @@ class RendererTestCase(unittest.TestCase): actual = self._read(renderer, filename) self.assertEquals(actual, 'non-ascii: ') - ## Test the _make_loader() method. + ## Test the _make_locator() method. - def test__make_loader__return_type(self): + def test__make_locator__return_type(self): """ - Test that _make_loader() returns a Loader. + Test that _make_locator() returns a Locator. """ renderer = Renderer() - loader = renderer._make_loader() + locator = renderer._make_locator() - self.assertEquals(type(loader), Loader) + self.assertEquals(type(locator), Locator) - def test__make_loader__file_encoding(self): + def test__make_locator__file_extension(self): """ - Test that _make_loader() respects the file_encoding attribute. - - """ - renderer = Renderer() - renderer.file_encoding = 'foo' - - loader = renderer._make_loader() - - self.assertEquals(loader.reader.encoding, 'foo') - - def test__make_loader__decode_errors(self): - """ - Test that _make_loader() respects the decode_errors attribute. - - """ - renderer = Renderer() - renderer.decode_errors = 'foo' - - loader = renderer._make_loader() - - self.assertEquals(loader.reader.decode_errors, 'foo') - - def test__make_loader__file_extension(self): - """ - Test that _make_loader() respects the file_extension attribute. + Test that _make_locator() respects the file_extension attribute. """ renderer = Renderer() renderer.file_extension = 'foo' - loader = renderer._make_loader() + locator = renderer._make_locator() - self.assertEquals(loader.template_extension, 'foo') + self.assertEquals(locator.template_extension, 'foo') - def test__make_loader__search_dirs(self): + def test__make_locator__search_dirs(self): """ - Test that _make_loader() respects the search_dirs attribute. + Test that _make_locator() respects the search_dirs attribute. """ renderer = Renderer() renderer.search_dirs = ['foo'] - loader = renderer._make_loader() + locator = renderer._make_locator() - self.assertEquals(loader.search_dirs, ['foo']) + self.assertEquals(locator.search_dirs, ['foo']) # This test is a sanity check. Strictly speaking, it shouldn't # be necessary based on our tests above. - def test__make_loader__default(self): + def test__make_locator__default(self): renderer = Renderer() - actual = renderer._make_loader() + actual = renderer._make_locator() - expected = Loader() + expected = Locator() self.assertEquals(type(actual), type(expected)) self.assertEquals(actual.template_extension, expected.template_extension) self.assertEquals(actual.search_dirs, expected.search_dirs) - self.assertEquals(actual.reader.__dict__, expected.reader.__dict__) ## Test the render() method. diff --git a/tests/test_view.py b/tests/test_view.py index 3f828b2..b71a5cc 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -42,13 +42,13 @@ class ViewTestCase(unittest.TestCase): view = Simple("Hi {{thing}}!", thing='world') self.assertEquals(view.render(), "Hi world!") - def test_template_load(self): + def test_render(self): view = Simple(thing='world') self.assertEquals(view.render(), "Hi world!") - def test_load_template__custom_loader(self): + def test_render__partials(self): """ - Test passing a custom loader to View.__init__(). + Test passing partials to View.__init__(). """ template = "{{>partial}}" -- cgit v1.2.1 From c76e2345b1148776450f26e1c9a383a7cafa554a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:35:46 -0800 Subject: A couple docstring tweaks. --- pystache/renderengine.py | 2 +- pystache/view.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index f51bff5..f636e2a 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -48,7 +48,7 @@ class RenderEngine(object): """ Provides a render() method. - This class is meant only for internal use by the Template class. + This class is meant only for internal use. As a rule, the code in this class operates on unicode strings where possible rather than, say, strings of type str or markupsafe.Markup. diff --git a/pystache/view.py b/pystache/view.py index 4507404..cf692c5 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -26,12 +26,12 @@ class View(object): Arguments: - partials: the object (e.g. pystache.Loader or dictionary) - responsible for loading partials during the rendering process. - The object should have a get() method that accepts a string and - returns the corresponding template as a string, preferably as a - unicode string. The method should return None if there is no - template with that name, or raise an exception. + partials: a custom object (e.g. dictionary) responsible for + loading partials during the rendering process. The object + should have a get() method that accepts a string and returns + the corresponding template as a string, preferably as a + unicode string. The method should return None if there is + no template with that name, or raise an exception. """ if template is not None: -- cgit v1.2.1 From 031186757b8926be2f5b86dff1bab8ffff7efc9f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:39:57 -0800 Subject: Added two test files left out of a previous commit. --- tests/data/duplicate.mustache | 1 + tests/data/locator/duplicate.mustache | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/data/duplicate.mustache create mode 100644 tests/data/locator/duplicate.mustache diff --git a/tests/data/duplicate.mustache b/tests/data/duplicate.mustache new file mode 100644 index 0000000..a0515e3 --- /dev/null +++ b/tests/data/duplicate.mustache @@ -0,0 +1 @@ +This file is used to test locate_path()'s search order. \ No newline at end of file diff --git a/tests/data/locator/duplicate.mustache b/tests/data/locator/duplicate.mustache new file mode 100644 index 0000000..a0515e3 --- /dev/null +++ b/tests/data/locator/duplicate.mustache @@ -0,0 +1 @@ +This file is used to test locate_path()'s search order. \ No newline at end of file -- cgit v1.2.1 From 7dc18cb1016228ece342e6e0ad0f35a1d4cd0325 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Dec 2011 17:41:17 -0800 Subject: Renamed loader.py to locator.py. --- pystache/loader.py | 93 ----------------------------------------- pystache/locator.py | 93 +++++++++++++++++++++++++++++++++++++++++ pystache/renderer.py | 4 +- pystache/view.py | 2 +- tests/test_loader.py | 109 ------------------------------------------------- tests/test_locator.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_renderer.py | 2 +- 7 files changed, 206 insertions(+), 206 deletions(-) delete mode 100644 pystache/loader.py create mode 100644 pystache/locator.py delete mode 100644 tests/test_loader.py create mode 100644 tests/test_locator.py diff --git a/pystache/loader.py b/pystache/loader.py deleted file mode 100644 index ee041df..0000000 --- a/pystache/loader.py +++ /dev/null @@ -1,93 +0,0 @@ -# coding: utf-8 - -""" -This module provides a Locator class. - -""" - -import os -import re -import sys - - -DEFAULT_EXTENSION = 'mustache' - - -def make_template_name(obj): - """ - Return the canonical template name for an object instance. - - This method converts Python-style class names (PEP 8's recommended - CamelCase, aka CapWords) to lower_case_with_underscords. Here - is an example with code: - - >>> class HelloWorld(object): - ... pass - >>> hi = HelloWorld() - >>> make_template_name(hi) - 'hello_world' - - """ - template_name = obj.__class__.__name__ - - def repl(match): - return '_' + match.group(0).lower() - - return re.sub('[A-Z]', repl, template_name)[1:] - - -class Locator(object): - - def __init__(self, search_dirs=None, extension=None): - """ - Construct a template locator. - - Arguments: - - search_dirs: the list of directories in which to search for templates, - for example when looking for partials. Defaults to the current - working directory. If given a string, the string is interpreted - as a single directory. - - extension: the template file extension. Defaults to "mustache". - Pass False for no extension (i.e. extensionless template files). - - """ - if extension is None: - extension = DEFAULT_EXTENSION - - if search_dirs is None: - search_dirs = os.curdir # i.e. "." - - if isinstance(search_dirs, basestring): - search_dirs = [search_dirs] - - self.search_dirs = search_dirs - self.template_extension = extension - - def make_file_name(self, template_name): - file_name = template_name - if self.template_extension is not False: - file_name += os.path.extsep + self.template_extension - - return file_name - - def locate_path(self, template_name): - """ - Find and return the path to the template with the given name. - - Raises an IOError if the template cannot be found. - - """ - search_dirs = self.search_dirs - - file_name = self.make_file_name(template_name) - - for dir_path in search_dirs: - file_path = os.path.join(dir_path, file_name) - if os.path.exists(file_path): - return file_path - - # TODO: we should probably raise an exception of our own type. - raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) - diff --git a/pystache/locator.py b/pystache/locator.py new file mode 100644 index 0000000..ee041df --- /dev/null +++ b/pystache/locator.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" +This module provides a Locator class. + +""" + +import os +import re +import sys + + +DEFAULT_EXTENSION = 'mustache' + + +def make_template_name(obj): + """ + Return the canonical template name for an object instance. + + This method converts Python-style class names (PEP 8's recommended + CamelCase, aka CapWords) to lower_case_with_underscords. Here + is an example with code: + + >>> class HelloWorld(object): + ... pass + >>> hi = HelloWorld() + >>> make_template_name(hi) + 'hello_world' + + """ + template_name = obj.__class__.__name__ + + def repl(match): + return '_' + match.group(0).lower() + + return re.sub('[A-Z]', repl, template_name)[1:] + + +class Locator(object): + + def __init__(self, search_dirs=None, extension=None): + """ + Construct a template locator. + + Arguments: + + search_dirs: the list of directories in which to search for templates, + for example when looking for partials. Defaults to the current + working directory. If given a string, the string is interpreted + as a single directory. + + extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + + """ + if extension is None: + extension = DEFAULT_EXTENSION + + if search_dirs is None: + search_dirs = os.curdir # i.e. "." + + if isinstance(search_dirs, basestring): + search_dirs = [search_dirs] + + self.search_dirs = search_dirs + self.template_extension = extension + + def make_file_name(self, template_name): + file_name = template_name + if self.template_extension is not False: + file_name += os.path.extsep + self.template_extension + + return file_name + + def locate_path(self, template_name): + """ + Find and return the path to the template with the given name. + + Raises an IOError if the template cannot be found. + + """ + search_dirs = self.search_dirs + + file_name = self.make_file_name(template_name) + + for dir_path in search_dirs: + file_path = os.path.join(dir_path, file_name) + if os.path.exists(file_path): + return file_path + + # TODO: we should probably raise an exception of our own type. + raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) + diff --git a/pystache/renderer.py b/pystache/renderer.py index c286d5f..4b70aa8 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -10,8 +10,8 @@ import os import sys from .context import Context -from .loader import DEFAULT_EXTENSION -from .loader import Locator +from .locator import DEFAULT_EXTENSION +from .locator import Locator from .reader import Reader from .renderengine import RenderEngine diff --git a/pystache/view.py b/pystache/view.py index cf692c5..2fc4a2f 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -6,7 +6,7 @@ This module provides a View class. """ from .context import Context -from .loader import make_template_name +from .locator import make_template_name from .renderer import Renderer diff --git a/tests/test_loader.py b/tests/test_loader.py deleted file mode 100644 index 7dc726e..0000000 --- a/tests/test_loader.py +++ /dev/null @@ -1,109 +0,0 @@ -# encoding: utf-8 - -""" -Contains loader.py unit tests. - -""" - -import os -import sys -import unittest - -from pystache.loader import make_template_name -from pystache.loader import Locator -from pystache.reader import Reader - -from .common import DATA_DIR - - -class MakeTemplateNameTests(unittest.TestCase): - - """ - Test the make_template_name() function. - - """ - - def test(self): - class FooBar(object): - pass - foo = FooBar() - self.assertEquals(make_template_name(foo), 'foo_bar') - - -class LocatorTests(unittest.TestCase): - - search_dirs = 'examples' - - def _locator(self): - return Locator(search_dirs=DATA_DIR) - - def test_init__search_dirs(self): - # Test the default value. - locator = Locator() - self.assertEquals(locator.search_dirs, [os.curdir]) - - locator = Locator(search_dirs=['foo']) - self.assertEquals(locator.search_dirs, ['foo']) - - def test_init__extension(self): - # Test the default value. - locator = Locator() - self.assertEquals(locator.template_extension, 'mustache') - - locator = Locator(extension='txt') - self.assertEquals(locator.template_extension, 'txt') - - locator = Locator(extension=False) - self.assertTrue(locator.template_extension is False) - - def test_make_file_name(self): - locator = Locator() - - locator.template_extension = 'bar' - self.assertEquals(locator.make_file_name('foo'), 'foo.bar') - - locator.template_extension = False - self.assertEquals(locator.make_file_name('foo'), 'foo') - - locator.template_extension = '' - self.assertEquals(locator.make_file_name('foo'), 'foo.') - - def test_locate_path(self): - locator = Locator(search_dirs='examples') - path = locator.locate_path('simple') - - self.assertEquals(os.path.basename(path), 'simple.mustache') - - def test_locate_path__using_list_of_paths(self): - locator = Locator(search_dirs=['doesnt_exist', 'examples']) - path = locator.locate_path('simple') - - self.assertTrue(path) - - def test_locate_path__precedence(self): - """ - Test the order in which locate_path() searches directories. - - """ - locator = Locator() - - dir1 = DATA_DIR - dir2 = os.path.join(DATA_DIR, 'locator') - - locator.search_dirs = [dir1] - self.assertTrue(locator.locate_path('duplicate')) - locator.search_dirs = [dir2] - self.assertTrue(locator.locate_path('duplicate')) - - locator.search_dirs = [dir2, dir1] - path = locator.locate_path('duplicate') - dirpath = os.path.dirname(path) - dirname = os.path.split(dirpath)[-1] - - self.assertEquals(dirname, 'locator') - - def test_locate_path__non_existent_template_fails(self): - locator = Locator() - - self.assertRaises(IOError, locator.locate_path, 'doesnt_exist') - diff --git a/tests/test_locator.py b/tests/test_locator.py new file mode 100644 index 0000000..5d0df1b --- /dev/null +++ b/tests/test_locator.py @@ -0,0 +1,109 @@ +# encoding: utf-8 + +""" +Contains locator.py unit tests. + +""" + +import os +import sys +import unittest + +from pystache.locator import make_template_name +from pystache.locator import Locator +from pystache.reader import Reader + +from .common import DATA_DIR + + +class MakeTemplateNameTests(unittest.TestCase): + + """ + Test the make_template_name() function. + + """ + + def test(self): + class FooBar(object): + pass + foo = FooBar() + self.assertEquals(make_template_name(foo), 'foo_bar') + + +class LocatorTests(unittest.TestCase): + + search_dirs = 'examples' + + def _locator(self): + return Locator(search_dirs=DATA_DIR) + + def test_init__search_dirs(self): + # Test the default value. + locator = Locator() + self.assertEquals(locator.search_dirs, [os.curdir]) + + locator = Locator(search_dirs=['foo']) + self.assertEquals(locator.search_dirs, ['foo']) + + def test_init__extension(self): + # Test the default value. + locator = Locator() + self.assertEquals(locator.template_extension, 'mustache') + + locator = Locator(extension='txt') + self.assertEquals(locator.template_extension, 'txt') + + locator = Locator(extension=False) + self.assertTrue(locator.template_extension is False) + + def test_make_file_name(self): + locator = Locator() + + locator.template_extension = 'bar' + self.assertEquals(locator.make_file_name('foo'), 'foo.bar') + + locator.template_extension = False + self.assertEquals(locator.make_file_name('foo'), 'foo') + + locator.template_extension = '' + self.assertEquals(locator.make_file_name('foo'), 'foo.') + + def test_locate_path(self): + locator = Locator(search_dirs='examples') + path = locator.locate_path('simple') + + self.assertEquals(os.path.basename(path), 'simple.mustache') + + def test_locate_path__using_list_of_paths(self): + locator = Locator(search_dirs=['doesnt_exist', 'examples']) + path = locator.locate_path('simple') + + self.assertTrue(path) + + def test_locate_path__precedence(self): + """ + Test the order in which locate_path() searches directories. + + """ + locator = Locator() + + dir1 = DATA_DIR + dir2 = os.path.join(DATA_DIR, 'locator') + + locator.search_dirs = [dir1] + self.assertTrue(locator.locate_path('duplicate')) + locator.search_dirs = [dir2] + self.assertTrue(locator.locate_path('duplicate')) + + locator.search_dirs = [dir2, dir1] + path = locator.locate_path('duplicate') + dirpath = os.path.dirname(path) + dirname = os.path.split(dirpath)[-1] + + self.assertEquals(dirname, 'locator') + + def test_locate_path__non_existent_template_fails(self): + locator = Locator() + + self.assertRaises(IOError, locator.locate_path, 'doesnt_exist') + diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 3fb5b59..d05bd19 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -12,7 +12,7 @@ import unittest from pystache import renderer from pystache.renderer import Renderer -from pystache.loader import Locator +from pystache.locator import Locator from .common import get_data_path -- cgit v1.2.1 From 5d2fe4f2e990ba05a89f1a487fd533a4fb463056 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 07:56:28 -0800 Subject: Created Renderer._render_string(). --- pystache/renderer.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 4b70aa8..b48ac43 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -255,6 +255,21 @@ class Renderer(object): load_template = self._make_load_template() return load_template(template_name) + def _render_string(self, template, *context, **kwargs): + """ + Render the given template string using the given context. + + """ + # RenderEngine.render() requires that the template string be unicode. + template = self._to_unicode_hard(template) + + context = Context.create(*context, **kwargs) + + engine = self._make_render_engine() + rendered = engine.render(template, context) + + return unicode(rendered) + def render_path(self, template_path, *context, **kwargs): """ Render the template at the given path using the given context. @@ -290,12 +305,5 @@ class Renderer(object): all items in the *context list. """ - engine = self._make_render_engine() - context = Context.create(*context, **kwargs) - - # RenderEngine.render() requires that the template string be unicode. - template = self._to_unicode_hard(template) + return self._render_string(template, *context, **kwargs) - rendered = engine.render(template, context) - - return unicode(rendered) -- cgit v1.2.1 From 708d6fb0cbe485b318eb8015c0d2b5fa8ea2fa9e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 17:43:54 -0800 Subject: Stubbed out Renderer.get_associated_template() method. --- pystache/renderer.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index b48ac43..2b319e9 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -255,6 +255,16 @@ class Renderer(object): load_template = self._make_load_template() return load_template(template_name) + def get_associated_template(self, obj): + """ + Find and return the template associated with an object. + + TODO: document this. + + """ + # TODO: implement this. + raise NotImplementedError() + def _render_string(self, template, *context, **kwargs): """ Render the given template string using the given context. @@ -282,16 +292,22 @@ class Renderer(object): def render(self, template, *context, **kwargs): """ - Render the given template using the given context. + Render the given template (or templated object) using the given context. + + Returns the rendering as a unicode string. - Returns a unicode string. + Prior to rendering, templates of type str are converted to unicode + using the default_encoding and decode_errors attributes. See the + constructor docstring for more information. Arguments: - template: a template string that is either unicode or of type str. - If the string has type str, it is first converted to unicode - using this instance's default_encoding and decode_errors - attributes. See the constructor docstring for more information. + template: a template string of type unicode or str, or an object + instance. If the argument is an object, the function attempts + to find a template associated to the object by calling the + get_associated_template() method. The object is also used as + the first element of the context stack when rendering this + associated template. *context: zero or more dictionaries, Context instances, or objects with which to populate the initial context stack. None @@ -305,5 +321,9 @@ class Renderer(object): all items in the *context list. """ - return self._render_string(template, *context, **kwargs) + if not isinstance(template, basestring): + # Then we assume the template is an object instance. + context = [template] + list(context) + template = self.get_associated_template(template) + return self._render_string(template, *context, **kwargs) -- cgit v1.2.1 From 8e9e39018f2c7786cb864a0b60e49760ea3876a6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 17:49:12 -0800 Subject: Tweaked docstring. --- pystache/renderer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 2b319e9..e482197 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -303,11 +303,11 @@ class Renderer(object): Arguments: template: a template string of type unicode or str, or an object - instance. If the argument is an object, the function attempts - to find a template associated to the object by calling the - get_associated_template() method. The object is also used as - the first element of the context stack when rendering this - associated template. + instance. If the argument is an object, for the template string + the function attempts to find a template associated to the + object by calling the get_associated_template() method. The + argument in this case is also used as the first element of the + context stack when rendering the associated template. *context: zero or more dictionaries, Context instances, or objects with which to populate the initial context stack. None -- cgit v1.2.1 From 3caf64c87685315e9d135a9a442fecc0916851ae Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 17:50:28 -0800 Subject: Got the spec tests working again (keyword argument loader -> partials). --- tests/spec_cases.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/spec_cases.py b/tests/spec_cases.py index 480aecd..3d34e18 100644 --- a/tests/spec_cases.py +++ b/tests/spec_cases.py @@ -46,8 +46,9 @@ def buildTest(testData, spec_filename): expected = testData['expected'] data = testData['data'] - renderer = Renderer(loader=partials) - actual = renderer.render(template, data).encode('utf-8') + renderer = Renderer(partials=partials) + actual = renderer.render(template, data) + actual = actual.encode('utf-8') message = """%s -- cgit v1.2.1 From 3b47fddce9d07e7e3a5fc74809a9ad71c6a780a5 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 18:17:40 -0800 Subject: Sketched out how to implement Renderer.get_associated_template(). --- pystache/renderer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index e482197..47edb46 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -262,7 +262,13 @@ class Renderer(object): TODO: document this. """ - # TODO: implement this. + # TODO: implement this as follows: + # + # (1) call self.locator.make_template_name(obj) + # (2) call self.locator.get_director(obj) + # (3) call self.locator.locate_path() with template_name argument + # and enlarged search_dirs. + # (4) call self.read(), and return the result. raise NotImplementedError() def _render_string(self, template, *context, **kwargs): -- cgit v1.2.1 From 04176645336e68e031592f523f9e459df6d1d7af Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 18:27:00 -0800 Subject: Added Renderer._render_object() method. --- pystache/renderer.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 47edb46..0f83cb1 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -286,6 +286,16 @@ class Renderer(object): return unicode(rendered) + def _render_object(self, obj, *context, **kwargs): + """ + Render the template associated with the given object. + + """ + context = [obj] + list(context) + template = self.get_associated_template(obj) + + return self._render_string(template, *context, **kwargs) + def render_path(self, template_path, *context, **kwargs): """ Render the template at the given path using the given context. @@ -294,7 +304,8 @@ class Renderer(object): """ template = self.read(template_path) - return self.render(template, *context, **kwargs) + + return self._render_string(template, *context, **kwargs) def render(self, template, *context, **kwargs): """ @@ -328,8 +339,7 @@ class Renderer(object): """ if not isinstance(template, basestring): - # Then we assume the template is an object instance. - context = [template] + list(context) - template = self.get_associated_template(template) + # Then we assume the template is an object. + return self._render_object(template, *context, **kwargs) return self._render_string(template, *context, **kwargs) -- cgit v1.2.1 From d7475e633beef4a2d62ec4bf5ca593653a3cdda9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 20:19:33 -0800 Subject: Refactoring of Locator class: make_template_name() and locate_path(). Changed make_template_name() from a function into a method. Moved the search_dirs argument from a Locator constructor argument to an argument of Locator.locate_path(). --- pystache/locator.py | 71 ++++++++++++++++++++++---------------------------- pystache/renderer.py | 8 +++--- pystache/view.py | 6 +++-- tests/test_locator.py | 54 ++++++++++++++------------------------ tests/test_renderer.py | 31 +++++++--------------- 5 files changed, 68 insertions(+), 102 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index ee041df..a609204 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module provides a Locator class. +This module provides a Locator class for finding template files. """ @@ -13,42 +13,14 @@ import sys DEFAULT_EXTENSION = 'mustache' -def make_template_name(obj): - """ - Return the canonical template name for an object instance. - - This method converts Python-style class names (PEP 8's recommended - CamelCase, aka CapWords) to lower_case_with_underscords. Here - is an example with code: - - >>> class HelloWorld(object): - ... pass - >>> hi = HelloWorld() - >>> make_template_name(hi) - 'hello_world' - - """ - template_name = obj.__class__.__name__ - - def repl(match): - return '_' + match.group(0).lower() - - return re.sub('[A-Z]', repl, template_name)[1:] - - class Locator(object): - def __init__(self, search_dirs=None, extension=None): + def __init__(self, extension=None): """ Construct a template locator. Arguments: - search_dirs: the list of directories in which to search for templates, - for example when looking for partials. Defaults to the current - working directory. If given a string, the string is interpreted - as a single directory. - extension: the template file extension. Defaults to "mustache". Pass False for no extension (i.e. extensionless template files). @@ -56,13 +28,6 @@ class Locator(object): if extension is None: extension = DEFAULT_EXTENSION - if search_dirs is None: - search_dirs = os.curdir # i.e. "." - - if isinstance(search_dirs, basestring): - search_dirs = [search_dirs] - - self.search_dirs = search_dirs self.template_extension = extension def make_file_name(self, template_name): @@ -72,15 +37,41 @@ class Locator(object): return file_name - def locate_path(self, template_name): + def make_template_name(self, obj): + """ + Return the canonical template name for an object instance. + + This method converts Python-style class names (PEP 8's recommended + CamelCase, aka CapWords) to lower_case_with_underscords. Here + is an example with code: + + >>> class HelloWorld(object): + ... pass + >>> hi = HelloWorld() + >>> + >>> locator = Locator() + >>> locator.make_template_name(hi) + 'hello_world' + + """ + template_name = obj.__class__.__name__ + + def repl(match): + return '_' + match.group(0).lower() + + return re.sub('[A-Z]', repl, template_name)[1:] + + def locate_path(self, template_name, search_dirs): """ Find and return the path to the template with the given name. Raises an IOError if the template cannot be found. - """ - search_dirs = self.search_dirs + Arguments: + search_dirs: the list of directories in which to search for templates. + + """ file_name = self.make_file_name(template_name) for dir_path in search_dirs: diff --git a/pystache/renderer.py b/pystache/renderer.py index 0f83cb1..2f32e7a 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -174,12 +174,12 @@ class Renderer(object): """ return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) - def _make_locator(self): + def make_locator(self): """ Create a Locator instance using current attributes. """ - return Locator(search_dirs=self.search_dirs, extension=self.file_extension) + return Locator(extension=self.file_extension) def _make_load_template(self): """ @@ -187,10 +187,10 @@ class Renderer(object): """ reader = self._make_reader() - locator = self._make_locator() + locator = self.make_locator() def load_template(template_name): - path = locator.locate_path(template_name) + path = locator.locate_path(template_name=template_name, search_dirs=self.search_dirs) return reader.read(path) return load_template diff --git a/pystache/view.py b/pystache/view.py index 2fc4a2f..ed97c63 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -6,7 +6,7 @@ This module provides a View class. """ from .context import Context -from .locator import make_template_name +from .locator import Locator from .renderer import Renderer @@ -20,6 +20,8 @@ class View(object): _renderer = None + locator = Locator() + def __init__(self, template=None, context=None, partials=None, **kwargs): """ Construct a View instance. @@ -85,7 +87,7 @@ class View(object): if self.template_name: return self.template_name - return make_template_name(self) + return self.locator.make_template_name(self) def render(self): """ diff --git a/tests/test_locator.py b/tests/test_locator.py index 5d0df1b..9524c15 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -9,27 +9,12 @@ import os import sys import unittest -from pystache.locator import make_template_name from pystache.locator import Locator from pystache.reader import Reader from .common import DATA_DIR -class MakeTemplateNameTests(unittest.TestCase): - - """ - Test the make_template_name() function. - - """ - - def test(self): - class FooBar(object): - pass - foo = FooBar() - self.assertEquals(make_template_name(foo), 'foo_bar') - - class LocatorTests(unittest.TestCase): search_dirs = 'examples' @@ -37,14 +22,6 @@ class LocatorTests(unittest.TestCase): def _locator(self): return Locator(search_dirs=DATA_DIR) - def test_init__search_dirs(self): - # Test the default value. - locator = Locator() - self.assertEquals(locator.search_dirs, [os.curdir]) - - locator = Locator(search_dirs=['foo']) - self.assertEquals(locator.search_dirs, ['foo']) - def test_init__extension(self): # Test the default value. locator = Locator() @@ -69,14 +46,14 @@ class LocatorTests(unittest.TestCase): self.assertEquals(locator.make_file_name('foo'), 'foo.') def test_locate_path(self): - locator = Locator(search_dirs='examples') - path = locator.locate_path('simple') + locator = Locator() + path = locator.locate_path('simple', search_dirs=['examples']) self.assertEquals(os.path.basename(path), 'simple.mustache') def test_locate_path__using_list_of_paths(self): - locator = Locator(search_dirs=['doesnt_exist', 'examples']) - path = locator.locate_path('simple') + locator = Locator() + path = locator.locate_path('simple', search_dirs=['doesnt_exist', 'examples']) self.assertTrue(path) @@ -90,13 +67,10 @@ class LocatorTests(unittest.TestCase): dir1 = DATA_DIR dir2 = os.path.join(DATA_DIR, 'locator') - locator.search_dirs = [dir1] - self.assertTrue(locator.locate_path('duplicate')) - locator.search_dirs = [dir2] - self.assertTrue(locator.locate_path('duplicate')) + self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir1])) + self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir2])) - locator.search_dirs = [dir2, dir1] - path = locator.locate_path('duplicate') + path = locator.locate_path('duplicate', search_dirs=[dir2, dir1]) dirpath = os.path.dirname(path) dirname = os.path.split(dirpath)[-1] @@ -105,5 +79,17 @@ class LocatorTests(unittest.TestCase): def test_locate_path__non_existent_template_fails(self): locator = Locator() - self.assertRaises(IOError, locator.locate_path, 'doesnt_exist') + self.assertRaises(IOError, locator.locate_path, 'doesnt_exist', search_dirs=[]) + + def test_make_template_name(self): + """ + Test make_template_name(). + + """ + locator = Locator() + + class FooBar(object): + pass + foo = FooBar() + self.assertEquals(locator.make_template_name(foo), 'foo_bar') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index d05bd19..3d355b1 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -216,53 +216,40 @@ class RendererTestCase(unittest.TestCase): actual = self._read(renderer, filename) self.assertEquals(actual, 'non-ascii: ') - ## Test the _make_locator() method. + ## Test the make_locator() method. - def test__make_locator__return_type(self): + def test_make_locator__return_type(self): """ - Test that _make_locator() returns a Locator. + Test that make_locator() returns a Locator. """ renderer = Renderer() - locator = renderer._make_locator() + locator = renderer.make_locator() self.assertEquals(type(locator), Locator) - def test__make_locator__file_extension(self): + def test_make_locator__file_extension(self): """ - Test that _make_locator() respects the file_extension attribute. + Test that make_locator() respects the file_extension attribute. """ renderer = Renderer() renderer.file_extension = 'foo' - locator = renderer._make_locator() + locator = renderer.make_locator() self.assertEquals(locator.template_extension, 'foo') - def test__make_locator__search_dirs(self): - """ - Test that _make_locator() respects the search_dirs attribute. - - """ - renderer = Renderer() - renderer.search_dirs = ['foo'] - - locator = renderer._make_locator() - - self.assertEquals(locator.search_dirs, ['foo']) - # This test is a sanity check. Strictly speaking, it shouldn't # be necessary based on our tests above. - def test__make_locator__default(self): + def test_make_locator__default(self): renderer = Renderer() - actual = renderer._make_locator() + actual = renderer.make_locator() expected = Locator() self.assertEquals(type(actual), type(expected)) self.assertEquals(actual.template_extension, expected.template_extension) - self.assertEquals(actual.search_dirs, expected.search_dirs) ## Test the render() method. -- cgit v1.2.1 From 2550417f2f57b9bb3ee6c8d7fda004126aebfb92 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 20:29:23 -0800 Subject: Refactored Locator: added _find_path() method. --- pystache/locator.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index a609204..2ef2fc5 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -30,6 +30,21 @@ class Locator(object): self.template_extension = extension + + def _find_path(self, file_name, search_dirs): + """ + Search for the given file, and return the path. + + Returns None if the file is not found. + + """ + for dir_path in search_dirs: + file_path = os.path.join(dir_path, file_name) + if os.path.exists(file_path): + return file_path + + return None + def make_file_name(self, template_name): file_name = template_name if self.template_extension is not False: @@ -74,11 +89,11 @@ class Locator(object): """ file_name = self.make_file_name(template_name) - for dir_path in search_dirs: - file_path = os.path.join(dir_path, file_name) - if os.path.exists(file_path): - return file_path + path = self._find_path(file_name, search_dirs) - # TODO: we should probably raise an exception of our own type. - raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),)) + if path is not None: + return path + # TODO: we should probably raise an exception of our own type. + raise IOError('Template %s not found in directories: %s' % + (repr(template_name), repr(search_dirs))) -- cgit v1.2.1 From cf4d98ef3030774eb5ecc556ce7cf823b2f60b0b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 20:33:21 -0800 Subject: Deleted a blank line. --- pystache/locator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pystache/locator.py b/pystache/locator.py index 2ef2fc5..4fe7bb4 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -88,7 +88,6 @@ class Locator(object): """ file_name = self.make_file_name(template_name) - path = self._find_path(file_name, search_dirs) if path is not None: -- cgit v1.2.1 From 383acd311767f884976eaf5fa4d919f8a61a3eea Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 21:16:17 -0800 Subject: Addressed issue #70: "Support Renderer.render(object, context)" --- pystache/locator.py | 21 ++++++++++++++------- pystache/renderer.py | 19 ++++++++++--------- tests/data/__init__.py | 0 tests/data/say_hello.mustache | 2 +- tests/data/templates.py | 7 +++++++ tests/test_locator.py | 10 ++++++++++ tests/test_renderer.py | 19 +++++++++++++++++-- 7 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 tests/data/__init__.py create mode 100644 tests/data/templates.py diff --git a/pystache/locator.py b/pystache/locator.py index 4fe7bb4..22c7eb9 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -30,7 +30,6 @@ class Locator(object): self.template_extension = extension - def _find_path(self, file_name, search_dirs): """ Search for the given file, and return the path. @@ -45,6 +44,20 @@ class Locator(object): return None + def get_object_directory(self, obj): + """ + Return the directory containing an object's defining class. + + """ + module = sys.modules[obj.__module__] + + # TODO: should we handle the case of __file__ not existing, for + # example when using the interpreter or using a module in the + # standard library)? + path = module.__file__ + + return os.path.dirname(path) + def make_file_name(self, template_name): file_name = template_name if self.template_extension is not False: @@ -80,12 +93,6 @@ class Locator(object): """ Find and return the path to the template with the given name. - Raises an IOError if the template cannot be found. - - Arguments: - - search_dirs: the list of directories in which to search for templates. - """ file_name = self.make_file_name(template_name) path = self._find_path(file_name, search_dirs) diff --git a/pystache/renderer.py b/pystache/renderer.py index 2f32e7a..5a0a512 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -259,17 +259,18 @@ class Renderer(object): """ Find and return the template associated with an object. - TODO: document this. + The function first searches the directory containing the object's + class definition. """ - # TODO: implement this as follows: - # - # (1) call self.locator.make_template_name(obj) - # (2) call self.locator.get_director(obj) - # (3) call self.locator.locate_path() with template_name argument - # and enlarged search_dirs. - # (4) call self.read(), and return the result. - raise NotImplementedError() + locator = self.make_locator() + + template_name = locator.make_template_name(obj) + directory = locator.get_object_directory(obj) + search_dirs = [directory] + self.search_dirs + path = locator.locate_path(template_name=template_name, search_dirs=search_dirs) + + return self.read(path) def _render_string(self, template, *context, **kwargs): """ diff --git a/tests/data/__init__.py b/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/say_hello.mustache b/tests/data/say_hello.mustache index 0824db0..84ab4c9 100644 --- a/tests/data/say_hello.mustache +++ b/tests/data/say_hello.mustache @@ -1 +1 @@ -Hello {{to}} \ No newline at end of file +Hello, {{to}} \ No newline at end of file diff --git a/tests/data/templates.py b/tests/data/templates.py new file mode 100644 index 0000000..ef629b9 --- /dev/null +++ b/tests/data/templates.py @@ -0,0 +1,7 @@ +# coding: utf-8 + + +class SayHello(object): + + def to(self): + return "World" diff --git a/tests/test_locator.py b/tests/test_locator.py index 9524c15..a7f3909 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -33,6 +33,16 @@ class LocatorTests(unittest.TestCase): locator = Locator(extension=False) self.assertTrue(locator.template_extension is False) + def test_get_object_directory(self): + locator = Locator() + + reader = Reader() + actual = locator.get_object_directory(reader) + + expected = os.path.join(os.path.dirname(__file__), os.pardir, 'pystache') + + self.assertEquals(os.path.normpath(actual), os.path.normpath(expected)) + def test_make_file_name(self): locator = Locator() diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 3d355b1..c7fe84d 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -15,6 +15,8 @@ from pystache.renderer import Renderer from pystache.locator import Locator from .common import get_data_path +from .data.templates import SayHello + class RendererInitTestCase(unittest.TestCase): @@ -375,9 +377,22 @@ class RendererTestCase(unittest.TestCase): """ renderer = Renderer() path = get_data_path('say_hello.mustache') - actual = renderer.render_path(path, to='world') - self.assertEquals(actual, "Hello world") + actual = renderer.render_path(path, to='foo') + self.assertEquals(actual, "Hello, foo") + + def test_render__object(self): + """ + Test rendering an object instance. + + """ + renderer = Renderer() + + say_hello = SayHello() + actual = renderer.render(say_hello) + self.assertEquals('Hello, World', actual) + actual = renderer.render(say_hello, to='Mars') + self.assertEquals('Hello, Mars', actual) # By testing that Renderer.render() constructs the right RenderEngine, # we no longer need to exercise all rendering code paths through -- cgit v1.2.1 From 7b7138b795c60a8872f6a7e1668160ca1b8ed51c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 21:25:28 -0800 Subject: Added test for rendering views via renderer.render(view). --- tests/test_renderer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index c7fe84d..6a58573 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -10,6 +10,7 @@ import os import sys import unittest +from examples.simple import Simple from pystache import renderer from pystache.renderer import Renderer from pystache.locator import Locator @@ -394,6 +395,18 @@ class RendererTestCase(unittest.TestCase): actual = renderer.render(say_hello, to='Mars') self.assertEquals('Hello, Mars', actual) + def test_render__view(self): + """ + Test rendering a View instance. + + """ + renderer = Renderer() + + view = Simple() + actual = renderer.render(view) + self.assertEquals('Hi pizza!', actual) + + # By testing that Renderer.render() constructs the right RenderEngine, # we no longer need to exercise all rendering code paths through # the Renderer. It suffices to test rendering paths through the -- cgit v1.2.1 From 7bddf0c52a0b692c433650bb07cdedb44ec584ce Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 22:50:19 -0800 Subject: Addressed issue #75: "Rudimentary benchmarking" Usage: tests/benchmark.py 10000 --- tests/benchmark.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 tests/benchmark.py diff --git a/tests/benchmark.py b/tests/benchmark.py new file mode 100755 index 0000000..2985f56 --- /dev/null +++ b/tests/benchmark.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +A rudimentary backward- and forward-compatible script to benchmark pystache. + +Usage: + +tests/benchmark.py 10000 + +""" + +import sys +from timeit import Timer + +import pystache + +# TODO: make the example realistic. + +examples = [ + # Test case: 1 + ("""\ +{{#person}}Hi {{name}}{{/person}}""", + {"person": {"name": "Jon"}}, + "Hi Jon"), +] + + +def make_test_function(example): + + template, context, expected = example + + def test(): + actual = pystache.render(template, context) + if actual != expected: + raise Exception("Benchmark mismatch") + + return test + + +def main(sys_argv): + args = sys_argv[1:] + count = int(args[0]) + + print "Benchmarking: %sx" % count + print + + for example in examples: + + test = make_test_function(example) + + t = Timer(test,) + print min(t.repeat(repeat=3, number=count)) + + print "Done" + + +if __name__ == '__main__': + main(sys.argv) + -- cgit v1.2.1 From e6cecddc789987c9e853a1a682664a8033f1ecf6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Dec 2011 23:11:39 -0800 Subject: Added a more substantial benchmarking test case. --- tests/benchmark.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/tests/benchmark.py b/tests/benchmark.py index 2985f56..d46e973 100755 --- a/tests/benchmark.py +++ b/tests/benchmark.py @@ -19,10 +19,44 @@ import pystache examples = [ # Test case: 1 - ("""\ -{{#person}}Hi {{name}}{{/person}}""", + ("""{{#person}}Hi {{name}}{{/person}}""", {"person": {"name": "Jon"}}, "Hi Jon"), + + # Test case: 2 + ("""\ +
+

{{header}}

+
    +{{#comments}}
  • +
    {{name}}

    {{body}}

    +
  • {{/comments}} +
+
""", + {'header': "My Post Comments", + 'comments': [ + {'name': "Joe", 'body': "Thanks for this post!"}, + {'name': "Sam", 'body': "Thanks for this post!"}, + {'name': "Heather", 'body': "Thanks for this post!"}, + {'name': "Kathy", 'body': "Thanks for this post!"}, + {'name': "George", 'body': "Thanks for this post!"}]}, + """\ +
+

My Post Comments

+
    +
  • +
    Joe

    Thanks for this post!

    +
  • +
    Sam

    Thanks for this post!

    +
  • +
    Heather

    Thanks for this post!

    +
  • +
    Kathy

    Thanks for this post!

    +
  • +
    George

    Thanks for this post!

    +
  • +
+
"""), ] @@ -33,7 +67,7 @@ def make_test_function(example): def test(): actual = pystache.render(template, context) if actual != expected: - raise Exception("Benchmark mismatch") + raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual)) return test -- cgit v1.2.1 From c602054f5a718fdb973e95da780d22d553be46e2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 14:01:33 -0800 Subject: Spec test display format: cherry-pick from '4eaa43b5379e1c8e14e97b6661a90e026bcb3a57' Improved display format of spec tests and fixed partials typo. --- tests/test_spec.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/tests/test_spec.py b/tests/test_spec.py index 125e411..2009710 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,10 +1,19 @@ +# coding: utf-8 + +""" +Tests the mustache spec test cases. + +""" + import glob import os.path -import pystache -from pystache import Loader import unittest import yaml +import pystache +from pystache import Loader + + def code_constructor(loader, node): value = loader.construct_mapping(node) return eval(value['python'], {}) @@ -17,7 +26,13 @@ specs = glob.glob(os.path.join(specs, '*.yml')) class MustacheSpec(unittest.TestCase): pass -def buildTest(testData, spec): +def buildTest(testData, spec_filename): + + name = testData['name'] + description = testData['desc'] + + test_name = "%s (%s)" % (name, spec_filename) + def test(self): template = testData['template'] partials = testData.has_key('partials') and testData['partials'] or {} @@ -32,19 +47,29 @@ def buildTest(testData, spec): p = open(files[-1], 'w') p.write(partials[key]) p.close() - self.assertEquals(pystache.render(template, data), expected) + actual = pystache.render(template, data).encode('utf-8') + + message = """%s + + Template: \"""%s\""" + + Expected: %s + Actual: %s""" % (description, template, repr(expected), repr(actual)) + + self.assertEquals(actual, expected, message) finally: [os.remove(f) for f in files] - test.__doc__ = testData['desc'] - test.__name__ = 'test %s (%s)' % (testData['name'], spec) + # The name must begin with "test" for nosetests test discovery to work. + test.__name__ = 'test: "%s"' % test_name + return test for spec in specs: - name = os.path.basename(spec).replace('.yml', '') + file_name = os.path.basename(spec) for test in yaml.load(open(spec))['tests']: - test = buildTest(test, name) + test = buildTest(test, file_name) setattr(MustacheSpec, test.__name__, test) if __name__ == '__main__': -- cgit v1.2.1 From 921cd28277c804e943679112440caed2fcd6a2c1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 12:24:23 -0800 Subject: Got doctests in README.rst working. --- README.rst | 15 ++++++++------- pystache/renderer.py | 12 ++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 769e8a4..5f4d192 100644 --- a/README.rst +++ b/README.rst @@ -37,16 +37,17 @@ Use It >>> import pystache >>> pystache.render('Hi {{person}}!', {'person': 'Mom'}) - 'Hi Mom!' + u'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" + >>> import pystache + >>> class Simple(pystache.View): + ... template_path = 'examples' + ... def thing(self): + ... return "pizza" Then your template, simple.mustache:: @@ -55,7 +56,7 @@ Then your template, simple.mustache:: Pull it together:: >>> Simple().render() - 'Hi pizza!' + u'Hi pizza!' Test It @@ -65,7 +66,7 @@ nose_ works great! :: pip install nose cd pystache - nosetests --with-doctest + nosetests --with-doctest --doctest-extension=rst To include tests from the mustache spec_ in your test runs: :: diff --git a/pystache/renderer.py b/pystache/renderer.py index 5a0a512..c17aa4f 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -310,7 +310,7 @@ class Renderer(object): def render(self, template, *context, **kwargs): """ - Render the given template (or templated object) using the given context. + Render the given template (or template object) using the given context. Returns the rendering as a unicode string. @@ -321,11 +321,11 @@ class Renderer(object): Arguments: template: a template string of type unicode or str, or an object - instance. If the argument is an object, for the template string - the function attempts to find a template associated to the - object by calling the get_associated_template() method. The - argument in this case is also used as the first element of the - context stack when rendering the associated template. + instance. If the argument is an object, the function first looks + for the template associated to the object by calling this class's + get_associated_template() method. The rendering process also + uses the passed object as the first element of the context stack + when rendering. *context: zero or more dictionaries, Context instances, or objects with which to populate the initial context stack. None -- cgit v1.2.1 From 4b8c7a702ded804b7f5300542d6945d6e92f4a32 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 12:41:58 -0800 Subject: Adjusted Locator.get_object_directory() to handle modules without '__file__'. --- pystache/locator.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index 22c7eb9..0467a76 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -48,12 +48,17 @@ class Locator(object): """ Return the directory containing an object's defining class. + Returns None if there is no such directory, for example if the + class was defined in an interactive Python session, or in a + doctest that appears in a text file (rather than a Python file). + """ module = sys.modules[obj.__module__] - # TODO: should we handle the case of __file__ not existing, for - # example when using the interpreter or using a module in the - # standard library)? + if not hasattr(module, '__file__'): + # TODO: add a unit test for this case. + return None + path = module.__file__ return os.path.dirname(path) -- cgit v1.2.1 From 9a57d05b9ee869a6913bffecc760566d27795561 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 12:47:58 -0800 Subject: Renderer.get_associated_template() now handles locator.get_object_directory() returning None. --- pystache/renderer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index c17aa4f..32bda26 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -263,11 +263,16 @@ class Renderer(object): class definition. """ + search_dirs = self.search_dirs locator = self.make_locator() template_name = locator.make_template_name(obj) + directory = locator.get_object_directory(obj) - search_dirs = [directory] + self.search_dirs + # TODO: add a unit test for the case of a None return value. + if directory is not None: + search_dirs = [directory] + self.search_dirs + path = locator.locate_path(template_name=template_name, search_dirs=search_dirs) return self.read(path) -- cgit v1.2.1 From 0995ca0d4e0cf271b8cfdc4675394fcafb49c097 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 12:50:59 -0800 Subject: Rewrote the Simple() example in README.rst without using pystache.View. --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 5f4d192..40ba330 100644 --- a/README.rst +++ b/README.rst @@ -43,9 +43,7 @@ You can also create dedicated view classes to hold your view logic. Here's your simple.py:: - >>> import pystache - >>> class Simple(pystache.View): - ... template_path = 'examples' + >>> class Simple(object): ... def thing(self): ... return "pizza" @@ -55,7 +53,8 @@ Then your template, simple.mustache:: Pull it together:: - >>> Simple().render() + >>> renderer = pystache.Renderer(search_dirs='examples') + >>> renderer.render(Simple()) u'Hi pizza!' -- cgit v1.2.1 From e336a961922cea2fdbd4eaf4b506ca6ffc4f1586 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 13:27:14 -0800 Subject: Renamed call()'s argument "x" to "val". --- pystache/template.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 03a691e..cbc6188 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -2,18 +2,18 @@ import re import cgi import inspect -def call(view, x, template=None): - if callable(x): - (args, _, _, _) = inspect.getargspec(x) +def call(view, val, template=None): + if callable(val): + (args, _, _, _) = inspect.getargspec(val) if len(args) is 0: - x = x() + val = val() elif len(args) is 1 and args[0] == 'self': - x = x(view) + val = val(view) elif len(args) is 1: - x = x(template) + val = val(template) else: - x = x(view, template) - return unicode(x) + val = val(view, template) + return unicode(val) def parse(template, view, delims=('{{', '}}')): tmpl = Template(template) -- cgit v1.2.1 From a3cb38ba5df2a5573f76675fa54b685c00335a85 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 13:30:12 -0800 Subject: Made call()'s val argument the first argument. --- pystache/template.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index cbc6188..39f7364 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -2,7 +2,7 @@ import re import cgi import inspect -def call(view, val, template=None): +def call(val, view, template=None): if callable(val): (args, _, _, _) = inspect.getargspec(val) if len(args) is 0: @@ -24,7 +24,7 @@ def parse(template, view, delims=('{{', '}}')): def renderParseTree(parsed, view, template): n = len(parsed) - return ''.join(map(call, [view] * n, parsed, [template] * n)) + return ''.join(map(call, parsed, [view] * n, [template] * n)) def render(template, view, delims=('{{', '}}')): parseTree = parse(template, view, delims) @@ -44,7 +44,7 @@ def sectionTag(name, parsed, template, delims): if not data: return '' elif callable(data): - ast = parse(call(self, data, template), self, delims) + ast = parse(call(view=self, val=data, template=template), self, delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -74,7 +74,7 @@ def escapedTag(name, delims): def unescapedTag(name, delims): def func(self): - return unicode(render(call(self, self.get(name)), self)) + return unicode(render(call(view=self, val=self.get(name)), self)) return func class EndOfSection(Exception): -- cgit v1.2.1 From 1e1c110a4e62873c8f90c6f130df7b92c49a3485 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 13:54:36 -0800 Subject: Changed unescapedTag() function argument from "self" to "context". --- pystache/template.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 39f7364..9082c3f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -7,7 +7,7 @@ def call(val, view, template=None): (args, _, _, _) = inspect.getargspec(val) if len(args) is 0: val = val() - elif len(args) is 1 and args[0] == 'self': + elif len(args) is 1 and args[0] in ['self', 'context']: val = val(view) elif len(args) is 1: val = val(template) @@ -73,8 +73,9 @@ def escapedTag(name, delims): return func def unescapedTag(name, delims): - def func(self): - return unicode(render(call(view=self, val=self.get(name)), self)) + def func(context): + template = call(val=context.get(name), view=context) + return unicode(render(template, context)) return func class EndOfSection(Exception): -- cgit v1.2.1 From 81ad7526947da34bb64751ee1704f214029d333b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 14:38:15 -0800 Subject: Removed unused section argument from _parse(). --- pystache/template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 9082c3f..7d15f7e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -20,7 +20,7 @@ def parse(template, view, delims=('{{', '}}')): tmpl.view = view tmpl.otag, tmpl.ctag = delims tmpl._compile_regexps() - return tmpl._parse() + return tmpl._parse(template) def renderParseTree(parsed, view, template): n = len(parsed) @@ -114,7 +114,7 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) - def _parse(self, template=None, section=None, index=0): + def _parse(self, template, index=0): """Parse a template into a syntax tree.""" template = template != None and template or self.template @@ -173,7 +173,7 @@ class Template(object): buffer.append(partialTag(name, captures['whitespace'])) elif captures['tag'] in ['#', '^']: try: - self._parse(template, name, pos) + self._parse(template, index=pos) except EndOfSection as e: bufr = e.buffer tmpl = e.template -- cgit v1.2.1 From 444b0dc315db8da65d317c0f05cd93c108b28ea9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 15:06:07 -0800 Subject: 8 more unit tests passing: decrement argument count if val is an instance method. Now at-- Ran 160 tests in 0.471s FAILED (errors=1, failures=24) --- pystache/template.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 7d15f7e..ef6c399 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -1,15 +1,25 @@ import re import cgi import inspect +import types def call(val, view, template=None): if callable(val): (args, _, _, _) = inspect.getargspec(val) - if len(args) is 0: + + args_count = len(args) + + if not isinstance(val, types.FunctionType): + # Then val is an instance method. Subtract one from the + # argument count because Python will automatically prepend + # self to the argument list when calling. + args_count -=1 + + if args_count is 0: val = val() - elif len(args) is 1 and args[0] in ['self', 'context']: + elif args_count is 1 and args[0] in ['self', 'context']: val = val(view) - elif len(args) is 1: + elif args_count is 1: val = val(template) else: val = val(view, template) @@ -117,7 +127,6 @@ class Template(object): def _parse(self, template, index=0): """Parse a template into a syntax tree.""" - template = template != None and template or self.template buffer = [] pos = index -- cgit v1.2.1 From 9497c957d6d25f5e31208c5a4dc5bfdcfb5ea033 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 15:22:08 -0800 Subject: 6 more tests passing: handle call() returning None. Now at-- Ran 160 tests in 0.755s FAILED (errors=1, failures=18) --- pystache/template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ef6c399..32bfaea 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -23,6 +23,10 @@ def call(val, view, template=None): val = val(template) else: val = val(view, template) + + if val is None: + val = '' + return unicode(val) def parse(template, view, delims=('{{', '}}')): @@ -34,7 +38,8 @@ def parse(template, view, delims=('{{', '}}')): def renderParseTree(parsed, view, template): n = len(parsed) - return ''.join(map(call, parsed, [view] * n, [template] * n)) + parts = map(call, parsed, [view] * n, [template] * n) + return ''.join(parts) def render(template, view, delims=('{{', '}}')): parseTree = parse(template, view, delims) @@ -84,7 +89,8 @@ def escapedTag(name, delims): def unescapedTag(name, delims): def func(context): - template = call(val=context.get(name), view=context) + val = context.get(name) + template = call(val=val, view=context) return unicode(render(template, context)) return func -- cgit v1.2.1 From 70a105b330aef7e6a81f1619178782b1449e5f32 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 25 Dec 2011 15:22:09 -0800 Subject: One less test failure: deleted the local test variable to prevent a test-run error. Cherry-pick from: 'f8a5be7ea859738d736a123ecf9e8b454bf04b09' Now at-- Ran 159 tests in 0.738s FAILED (failures=18) --- tests/test_spec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_spec.py b/tests/test_spec.py index 2009710..46faff5 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -71,6 +71,8 @@ for spec in specs: for test in yaml.load(open(spec))['tests']: test = buildTest(test, file_name) setattr(MustacheSpec, test.__name__, test) + # Prevent this variable from being interpreted as another test. + del(test) if __name__ == '__main__': unittest.main() -- cgit v1.2.1 From 269f12ce1dc3d7b8aa16865a5db303e1c7467d4d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:15:43 -0800 Subject: 6 more tests passing: call lambdas. Now at-- Ran 159 tests in 0.502s FAILED (failures=12) --- pystache/template.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pystache/template.py b/pystache/template.py index 32bfaea..74e6641 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -24,6 +24,9 @@ def call(val, view, template=None): else: val = val(view, template) + if callable(val): + val = val(template) + if val is None: val = '' -- cgit v1.2.1 From c25003423373d7f3e94f151544c8bcea7ca6d394 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:27:24 -0800 Subject: Renamed _parse() to parse_to_tree(). --- pystache/template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 74e6641..edb1a92 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -37,7 +37,7 @@ def parse(template, view, delims=('{{', '}}')): tmpl.view = view tmpl.otag, tmpl.ctag = delims tmpl._compile_regexps() - return tmpl._parse(template) + return tmpl.parse_to_tree(template) def renderParseTree(parsed, view, template): n = len(parsed) @@ -133,7 +133,7 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) - def _parse(self, template, index=0): + def parse_to_tree(self, template, index=0): """Parse a template into a syntax tree.""" buffer = [] @@ -191,7 +191,7 @@ class Template(object): buffer.append(partialTag(name, captures['whitespace'])) elif captures['tag'] in ['#', '^']: try: - self._parse(template, index=pos) + self.parse_to_tree(template, index=pos) except EndOfSection as e: bufr = e.buffer tmpl = e.template -- cgit v1.2.1 From cb8c06ca370a0587fbed6355db3cbc248585f634 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:32:19 -0800 Subject: Renamed variables in the parse_to_tree() method. --- pystache/template.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index edb1a92..ccf93aa 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -134,23 +134,25 @@ class Template(object): self.tag_re = re.compile(tag % tags, re.M | re.X) def parse_to_tree(self, template, index=0): - """Parse a template into a syntax tree.""" + """ + Parse a template into a syntax tree. - buffer = [] - pos = index + """ + parse_tree = [] + start_index = index while True: - match = self.tag_re.search(template, pos) + match = self.tag_re.search(template, index) if match is None: break - pos = self._handle_match(template, match, buffer, index) + index = self._handle_match(template, match, parse_tree, start_index) # Save the rest of the template. - buffer.append(template[pos:]) + parse_tree.append(template[index:]) - return buffer + return parse_tree def _handle_match(self, template, match, buffer, index): # Normalize the captures dictionary. -- cgit v1.2.1 From 021ab20afff446f11b078b00653372f9e20f0e2c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:35:06 -0800 Subject: Renamed index to start_index. --- pystache/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ccf93aa..94a2cf8 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -154,7 +154,7 @@ class Template(object): return parse_tree - def _handle_match(self, template, match, buffer, index): + def _handle_match(self, template, match, buffer, start_index): # Normalize the captures dictionary. captures = match.groupdict() if captures['change'] is not None: @@ -202,7 +202,7 @@ class Template(object): tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] buffer.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': - raise EndOfSection(buffer, template[index:tagPos], pos) + raise EndOfSection(buffer, template[start_index:tagPos], pos) elif captures['tag'] in ['{', '&']: buffer.append(unescapedTag(name, (self.otag, self.ctag))) elif captures['tag'] == '': -- cgit v1.2.1 From e4175d4bc7c499664911acf7062d67b69ecbb399 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:50:33 -0800 Subject: Renamed "pos" to "end_index". --- pystache/template.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 94a2cf8..e7c2452 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -164,20 +164,20 @@ class Template(object): # Save the literal text content. buffer.append(captures['content']) - pos = match.end() + end_index = match.end() tagPos = match.end('content') # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. tagBeganLine = not tagPos or template[tagPos - 1] in ['\r', '\n'] - tagEndedLine = (pos == len(template) or template[pos] in ['\r', '\n']) + tagEndedLine = (end_index == len(template) or template[end_index] in ['\r', '\n']) interpolationTag = captures['tag'] in ['', '&', '{'] if (tagBeganLine and tagEndedLine and not interpolationTag): - if pos < len(template): - pos += template[pos] == '\r' and 1 or 0 - if pos < len(template): - pos += template[pos] == '\n' and 1 or 0 + if end_index < len(template): + end_index += template[end_index] == '\r' and 1 or 0 + if end_index < len(template): + end_index += template[end_index] == '\n' and 1 or 0 elif captures['whitespace']: buffer.append(captures['whitespace']) tagPos += len(captures['whitespace']) @@ -193,16 +193,16 @@ class Template(object): buffer.append(partialTag(name, captures['whitespace'])) elif captures['tag'] in ['#', '^']: try: - self.parse_to_tree(template, index=pos) + self.parse_to_tree(template, index=end_index) except EndOfSection as e: bufr = e.buffer tmpl = e.template - pos = e.position + end_index = e.position tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] buffer.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': - raise EndOfSection(buffer, template[start_index:tagPos], pos) + raise EndOfSection(buffer, template[start_index:tagPos], end_index) elif captures['tag'] in ['{', '&']: buffer.append(unescapedTag(name, (self.otag, self.ctag))) elif captures['tag'] == '': @@ -210,7 +210,7 @@ class Template(object): else: raise Exception("'%s' is an unrecognized type!" % captures['tag']) - return pos + return end_index def render(self, encoding=None): result = render(self.template, self.view) -- cgit v1.2.1 From 9613e38698caaadad297fbe623a5b2cf670a8e36 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:52:47 -0800 Subject: Renamed tagPos to match_index. --- pystache/template.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index e7c2452..0671426 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -165,11 +165,11 @@ class Template(object): # Save the literal text content. buffer.append(captures['content']) end_index = match.end() - tagPos = match.end('content') + match_index = match.end('content') # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. - tagBeganLine = not tagPos or template[tagPos - 1] in ['\r', '\n'] + tagBeganLine = not match_index or template[match_index - 1] in ['\r', '\n'] tagEndedLine = (end_index == len(template) or template[end_index] in ['\r', '\n']) interpolationTag = captures['tag'] in ['', '&', '{'] @@ -180,7 +180,7 @@ class Template(object): end_index += template[end_index] == '\n' and 1 or 0 elif captures['whitespace']: buffer.append(captures['whitespace']) - tagPos += len(captures['whitespace']) + match_index += len(captures['whitespace']) captures['whitespace'] = '' name = captures['name'] @@ -202,7 +202,7 @@ class Template(object): tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] buffer.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': - raise EndOfSection(buffer, template[start_index:tagPos], end_index) + raise EndOfSection(buffer, template[start_index:match_index], end_index) elif captures['tag'] in ['{', '&']: buffer.append(unescapedTag(name, (self.otag, self.ctag))) elif captures['tag'] == '': -- cgit v1.2.1 From b4f6f97d39bc4c677524db3e5649b7b53ce700ec Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 16:54:14 -0800 Subject: Renamed buffer to parse_tree. --- pystache/template.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 0671426..d8f12c4 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -98,8 +98,8 @@ def unescapedTag(name, delims): return func class EndOfSection(Exception): - def __init__(self, buffer, template, position): - self.buffer = buffer + def __init__(self, parse_tree, template, position): + self.parse_tree = parse_tree self.template = template self.position = position @@ -154,7 +154,7 @@ class Template(object): return parse_tree - def _handle_match(self, template, match, buffer, start_index): + def _handle_match(self, template, match, parse_tree, start_index): # Normalize the captures dictionary. captures = match.groupdict() if captures['change'] is not None: @@ -163,7 +163,7 @@ class Template(object): captures.update(tag='{', name=captures['raw_name']) # Save the literal text content. - buffer.append(captures['content']) + parse_tree.append(captures['content']) end_index = match.end() match_index = match.end('content') @@ -179,7 +179,7 @@ class Template(object): if end_index < len(template): end_index += template[end_index] == '\n' and 1 or 0 elif captures['whitespace']: - buffer.append(captures['whitespace']) + parse_tree.append(captures['whitespace']) match_index += len(captures['whitespace']) captures['whitespace'] = '' @@ -190,23 +190,23 @@ class Template(object): self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - buffer.append(partialTag(name, captures['whitespace'])) + parse_tree.append(partialTag(name, captures['whitespace'])) elif captures['tag'] in ['#', '^']: try: self.parse_to_tree(template, index=end_index) except EndOfSection as e: - bufr = e.buffer + bufr = e.parse_tree tmpl = e.template end_index = e.position tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] - buffer.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) + parse_tree.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': - raise EndOfSection(buffer, template[start_index:match_index], end_index) + raise EndOfSection(parse_tree, template[start_index:match_index], end_index) elif captures['tag'] in ['{', '&']: - buffer.append(unescapedTag(name, (self.otag, self.ctag))) + parse_tree.append(unescapedTag(name, (self.otag, self.ctag))) elif captures['tag'] == '': - buffer.append(escapedTag(name, (self.otag, self.ctag))) + parse_tree.append(escapedTag(name, (self.otag, self.ctag))) else: raise Exception("'%s' is an unrecognized type!" % captures['tag']) -- cgit v1.2.1 From 12f5ad2b36af9dd8b881b0bb17a8daf8eeb31796 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 17:05:58 -0800 Subject: Renamed some variables in _handle_match(). --- pystache/template.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index d8f12c4..988d97a 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -3,6 +3,10 @@ import cgi import inspect import types + +END_OF_LINE_CHARACTERS = ['\r', '\n'] + + def call(val, view, template=None): if callable(val): (args, _, _, _) = inspect.getargspec(val) @@ -162,18 +166,18 @@ class Template(object): elif captures['raw'] is not None: captures.update(tag='{', name=captures['raw_name']) - # Save the literal text content. parse_tree.append(captures['content']) - end_index = match.end() + match_index = match.end('content') + end_index = match.end() # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. - tagBeganLine = not match_index or template[match_index - 1] in ['\r', '\n'] - tagEndedLine = (end_index == len(template) or template[end_index] in ['\r', '\n']) - interpolationTag = captures['tag'] in ['', '&', '{'] + did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS + did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS + is_tag_interpolating = captures['tag'] in ['', '&', '{'] - if (tagBeganLine and tagEndedLine and not interpolationTag): + if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: if end_index < len(template): end_index += template[end_index] == '\r' and 1 or 0 if end_index < len(template): -- cgit v1.2.1 From b30e1c2a746e8a0ccd40f94c32770aaa34fcf31e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 17:13:48 -0800 Subject: Removed unused delims argument from escape and unescape functions. --- pystache/template.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 988d97a..9add9a2 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -88,13 +88,13 @@ def inverseTag(name, parsed, template, delims): return renderParseTree(parsed, self, delims) return func -def escapedTag(name, delims): - fetch = unescapedTag(name, delims) +def escape_tag_function(name): + fetch = literal_tag_function(name) def func(self): return cgi.escape(fetch(self), True) return func -def unescapedTag(name, delims): +def literal_tag_function(name): def func(context): val = context.get(name) template = call(val=val, view=context) @@ -194,23 +194,30 @@ class Template(object): self.otag, self.ctag = name.split() self._compile_regexps() elif captures['tag'] == '>': - parse_tree.append(partialTag(name, captures['whitespace'])) + func = partialTag(name, captures['whitespace']) + parse_tree.append(func) elif captures['tag'] in ['#', '^']: try: self.parse_to_tree(template, index=end_index) except EndOfSection as e: bufr = e.parse_tree tmpl = e.template - end_index = e.position + end_index = e.position tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] parse_tree.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) elif captures['tag'] == '/': raise EndOfSection(parse_tree, template[start_index:match_index], end_index) elif captures['tag'] in ['{', '&']: - parse_tree.append(unescapedTag(name, (self.otag, self.ctag))) + + func = literal_tag_function(name) + parse_tree.append(func) + elif captures['tag'] == '': - parse_tree.append(escapedTag(name, (self.otag, self.ctag))) + + func = escape_tag_function(name) + parse_tree.append(func) + else: raise Exception("'%s' is an unrecognized type!" % captures['tag']) -- cgit v1.2.1 From 8d3e051a600522347b726cc8e85889ff93341f2f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 17:22:49 -0800 Subject: Refactored render_parse_tree() method. --- pystache/template.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 9add9a2..411c28f 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -43,14 +43,15 @@ def parse(template, view, delims=('{{', '}}')): tmpl._compile_regexps() return tmpl.parse_to_tree(template) -def renderParseTree(parsed, view, template): - n = len(parsed) - parts = map(call, parsed, [view] * n, [template] * n) +def render_parse_tree(parse_tree, view, template): + get_string = lambda val: call(val, view, template) + parts = map(get_string, parse_tree) + return ''.join(parts) def render(template, view, delims=('{{', '}}')): parseTree = parse(template, view, delims) - return renderParseTree(parseTree, view, template) + return render_parse_tree(parseTree, view, template) def partialTag(name, indentation=''): def func(self): @@ -74,7 +75,7 @@ def sectionTag(name, parsed, template, delims): parts = [] for element in data: self.context_list.insert(0, element) - parts.append(renderParseTree(ast, self, delims)) + parts.append(render_parse_tree(ast, self, delims)) del self.context_list[0] return ''.join(parts) @@ -85,7 +86,7 @@ def inverseTag(name, parsed, template, delims): data = self.get(name) if data: return '' - return renderParseTree(parsed, self, delims) + return render_parse_tree(parsed, self, delims) return func def escape_tag_function(name): -- cgit v1.2.1 From 46ffcc2ecc1ccaa9cffef04ce753616ab93c4969 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 18:07:15 -0800 Subject: Removed an extraneous template argument Template.parse_to_tree(). --- pystache/template.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 411c28f..42f5767 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -36,12 +36,12 @@ def call(val, view, template=None): return unicode(val) -def parse(template, view, delims=('{{', '}}')): - tmpl = Template(template) - tmpl.view = view - tmpl.otag, tmpl.ctag = delims - tmpl._compile_regexps() - return tmpl.parse_to_tree(template) +def parse_to_tree(template, view, delims=('{{', '}}')): + template = Template(template) + template.view = view + template.otag, template.ctag = delims + template._compile_regexps() + return template.parse_to_tree() def render_parse_tree(parse_tree, view, template): get_string = lambda val: call(val, view, template) @@ -50,8 +50,8 @@ def render_parse_tree(parse_tree, view, template): return ''.join(parts) def render(template, view, delims=('{{', '}}')): - parseTree = parse(template, view, delims) - return render_parse_tree(parseTree, view, template) + parse_tree = parse_to_tree(template, view, delims) + return render_parse_tree(parse_tree, view, template) def partialTag(name, indentation=''): def func(self): @@ -60,14 +60,14 @@ def partialTag(name, indentation=''): return render(template, self) return func -def sectionTag(name, parsed, template, delims): +def sectionTag(name, parse_tree_, template, delims): def func(self): + parse_tree = parse_tree_ data = self.get(name) - ast = parsed if not data: return '' elif callable(data): - ast = parse(call(view=self, val=data, template=template), self, delims) + parse_tree = parse_to_tree(call(view=self, val=data, template=template), self, delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -75,7 +75,7 @@ def sectionTag(name, parsed, template, delims): parts = [] for element in data: self.context_list.insert(0, element) - parts.append(render_parse_tree(ast, self, delims)) + parts.append(render_parse_tree(parse_tree, self, delims)) del self.context_list[0] return ''.join(parts) @@ -138,12 +138,13 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) - def parse_to_tree(self, template, index=0): + def parse_to_tree(self, index=0): """ Parse a template into a syntax tree. """ parse_tree = [] + template = self.template start_index = index while True: @@ -199,7 +200,7 @@ class Template(object): parse_tree.append(func) elif captures['tag'] in ['#', '^']: try: - self.parse_to_tree(template, index=end_index) + self.parse_to_tree(index=end_index) except EndOfSection as e: bufr = e.parse_tree tmpl = e.template -- cgit v1.2.1 From 3da797f944c6f66ea79e4a4935c11478763e63f3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 18:52:55 -0800 Subject: More minor refactorings. --- pystache/template.py | 62 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 42f5767..a5c3f00 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -7,6 +7,7 @@ import types END_OF_LINE_CHARACTERS = ['\r', '\n'] +# TODO: what are the possibilities for val? def call(val, view, template=None): if callable(val): (args, _, _, _) = inspect.getargspec(val) @@ -44,15 +45,28 @@ def parse_to_tree(template, view, delims=('{{', '}}')): return template.parse_to_tree() def render_parse_tree(parse_tree, view, template): + """ + Convert a parse-tree into a string. + + """ get_string = lambda val: call(val, view, template) parts = map(get_string, parse_tree) return ''.join(parts) def render(template, view, delims=('{{', '}}')): + """ + Arguments: + + template: template string + view: context + + """ parse_tree = parse_to_tree(template, view, delims) return render_parse_tree(parse_tree, view, template) +## The possible function parse-tree elements: + def partialTag(name, indentation=''): def func(self): nonblank = re.compile(r'^(.)', re.M) @@ -60,14 +74,16 @@ def partialTag(name, indentation=''): return render(template, self) return func -def sectionTag(name, parse_tree_, template, delims): +def sectionTag(name, parse_tree_, template_, delims): def func(self): + template = template_ parse_tree = parse_tree_ data = self.get(name) if not data: return '' elif callable(data): - parse_tree = parse_to_tree(call(view=self, val=data, template=template), self, delims) + template = call(val=data, view=self, template=template) + parse_tree = parse_to_tree(template, self, delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -153,16 +169,21 @@ class Template(object): if match is None: break - index = self._handle_match(template, match, parse_tree, start_index) + captures = match.groupdict() + match_index = match.end('content') + end_index = match.end() + + index = self._handle_match(parse_tree, captures, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) return parse_tree - def _handle_match(self, template, match, parse_tree, start_index): + def _handle_match(self, parse_tree, captures, start_index, match_index, end_index): + template = self.template + # Normalize the captures dictionary. - captures = match.groupdict() if captures['change'] is not None: captures.update(tag='=', name=captures['delims']) elif captures['raw'] is not None: @@ -170,9 +191,6 @@ class Template(object): parse_tree.append(captures['content']) - match_index = match.end('content') - end_index = match.end() - # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS @@ -190,15 +208,19 @@ class Template(object): captures['whitespace'] = '' name = captures['name'] + if captures['tag'] == '!': - pass - elif captures['tag'] == '=': + return end_index + + if captures['tag'] == '=': self.otag, self.ctag = name.split() self._compile_regexps() - elif captures['tag'] == '>': + return end_index + + if captures['tag'] == '>': func = partialTag(name, captures['whitespace']) - parse_tree.append(func) elif captures['tag'] in ['#', '^']: + try: self.parse_to_tree(index=end_index) except EndOfSection as e: @@ -206,23 +228,27 @@ class Template(object): tmpl = e.template end_index = e.position - tag = { '#': sectionTag, '^': inverseTag }[captures['tag']] - parse_tree.append(tag(name, bufr, tmpl, (self.otag, self.ctag))) - elif captures['tag'] == '/': - raise EndOfSection(parse_tree, template[start_index:match_index], end_index) + tag = sectionTag if captures['tag'] == '#' else inverseTag + func = tag(name, bufr, tmpl, (self.otag, self.ctag)) + elif captures['tag'] in ['{', '&']: func = literal_tag_function(name) - parse_tree.append(func) elif captures['tag'] == '': func = escape_tag_function(name) - parse_tree.append(func) + + elif captures['tag'] == '/': + + # TODO: don't use exceptions for flow control. + raise EndOfSection(parse_tree, template[start_index:match_index], end_index) else: raise Exception("'%s' is an unrecognized type!" % captures['tag']) + parse_tree.append(func) + return end_index def render(self, encoding=None): -- cgit v1.2.1 From 5fea146b0cc9d3186669a6311ffb32ef7ff94e7f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 19:39:53 -0800 Subject: Moved escape and literal functions into Template class. --- pystache/template.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index a5c3f00..3c2236e 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -105,19 +105,6 @@ def inverseTag(name, parsed, template, delims): return render_parse_tree(parsed, self, delims) return func -def escape_tag_function(name): - fetch = literal_tag_function(name) - def func(self): - return cgi.escape(fetch(self), True) - return func - -def literal_tag_function(name): - def func(context): - val = context.get(name) - template = call(val=val, view=context) - return unicode(render(template, context)) - return func - class EndOfSection(Exception): def __init__(self, parse_tree, template, position): self.parse_tree = parse_tree @@ -154,6 +141,19 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) + def escape_tag_function(self, name): + fetch = self.literal_tag_function(name) + def func(context): + return cgi.escape(fetch(context), True) + return func + + def literal_tag_function(self, name): + def func(context): + val = context.get(name) + template = call(val=val, view=context) + return unicode(render(template, context)) + return func + def parse_to_tree(self, index=0): """ Parse a template into a syntax tree. @@ -233,11 +233,11 @@ class Template(object): elif captures['tag'] in ['{', '&']: - func = literal_tag_function(name) + func = self.literal_tag_function(name) elif captures['tag'] == '': - func = escape_tag_function(name) + func = self.escape_tag_function(name) elif captures['tag'] == '/': -- cgit v1.2.1 From 846b5b2a4cd86027efe395524b5caf92861e57d7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 19:43:13 -0800 Subject: Added to_unicode() and escape() methods to the Template class. --- pystache/template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 3c2236e..9ab13ad 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -141,17 +141,23 @@ class Template(object): """ self.tag_re = re.compile(tag % tags, re.M | re.X) + def to_unicode(self, text): + return unicode(text) + + def escape(self, text): + return cgi.escape(text, True) + def escape_tag_function(self, name): fetch = self.literal_tag_function(name) def func(context): - return cgi.escape(fetch(context), True) + return self.escape(fetch(context)) return func def literal_tag_function(self, name): def func(context): val = context.get(name) template = call(val=val, view=context) - return unicode(render(template, context)) + return self.to_unicode(render(template, context)) return func def parse_to_tree(self, index=0): -- cgit v1.2.1 From e6e84297e7ac05e63de229ec0d1afcd09686ca12 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 19:45:54 -0800 Subject: Moved partial tag function into Template. --- pystache/template.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index 9ab13ad..da36c3d 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -65,15 +65,6 @@ def render(template, view, delims=('{{', '}}')): parse_tree = parse_to_tree(template, view, delims) return render_parse_tree(parse_tree, view, template) -## The possible function parse-tree elements: - -def partialTag(name, indentation=''): - def func(self): - nonblank = re.compile(r'^(.)', re.M) - template = re.sub(nonblank, indentation + r'\1', self.partial(name)) - return render(template, self) - return func - def sectionTag(name, parse_tree_, template_, delims): def func(self): template = template_ @@ -160,6 +151,13 @@ class Template(object): return self.to_unicode(render(template, context)) return func + def partial_tag_function(self, name, indentation=''): + def func(context): + nonblank = re.compile(r'^(.)', re.M) + template = re.sub(nonblank, indentation + r'\1', context.partial(name)) + return render(template, context) + return func + def parse_to_tree(self, index=0): """ Parse a template into a syntax tree. @@ -224,7 +222,7 @@ class Template(object): return end_index if captures['tag'] == '>': - func = partialTag(name, captures['whitespace']) + func = self.partial_tag_function(name, captures['whitespace']) elif captures['tag'] in ['#', '^']: try: -- cgit v1.2.1 From 1b04089e1dbcd92ab68799eb6b7860152b63cb0d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 19:47:32 -0800 Subject: Added a partial() method to Template class. --- pystache/template.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pystache/template.py b/pystache/template.py index da36c3d..f848f67 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -138,6 +138,9 @@ class Template(object): def escape(self, text): return cgi.escape(text, True) + def partial(self, name, context=None): + return context.partial(name) + def escape_tag_function(self, name): fetch = self.literal_tag_function(name) def func(context): @@ -154,7 +157,7 @@ class Template(object): def partial_tag_function(self, name, indentation=''): def func(context): nonblank = re.compile(r'^(.)', re.M) - template = re.sub(nonblank, indentation + r'\1', context.partial(name)) + template = re.sub(nonblank, indentation + r'\1', self.partial(name, context)) return render(template, context) return func -- cgit v1.2.1 From ca0ad881a9994b6c0481d253d889933e8f80e7c3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Dec 2011 19:59:20 -0800 Subject: Moved more methods into Template class. --- pystache/template.py | 97 ++++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index f848f67..fadb422 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -37,13 +37,6 @@ def call(val, view, template=None): return unicode(val) -def parse_to_tree(template, view, delims=('{{', '}}')): - template = Template(template) - template.view = view - template.otag, template.ctag = delims - template._compile_regexps() - return template.parse_to_tree() - def render_parse_tree(parse_tree, view, template): """ Convert a parse-tree into a string. @@ -54,40 +47,6 @@ def render_parse_tree(parse_tree, view, template): return ''.join(parts) -def render(template, view, delims=('{{', '}}')): - """ - Arguments: - - template: template string - view: context - - """ - parse_tree = parse_to_tree(template, view, delims) - return render_parse_tree(parse_tree, view, template) - -def sectionTag(name, parse_tree_, template_, delims): - def func(self): - template = template_ - parse_tree = parse_tree_ - data = self.get(name) - if not data: - return '' - elif callable(data): - template = call(val=data, view=self, template=template) - parse_tree = parse_to_tree(template, self, delims) - data = [ data ] - elif type(data) not in [list, tuple]: - data = [ data ] - - parts = [] - for element in data: - self.context_list.insert(0, element) - parts.append(render_parse_tree(parse_tree, self, delims)) - del self.context_list[0] - - return ''.join(parts) - return func - def inverseTag(name, parsed, template, delims): def func(self): data = self.get(name) @@ -151,16 +110,53 @@ class Template(object): def func(context): val = context.get(name) template = call(val=val, view=context) - return self.to_unicode(render(template, context)) + return self.to_unicode(self.render_template(template, context)) return func def partial_tag_function(self, name, indentation=''): def func(context): nonblank = re.compile(r'^(.)', re.M) template = re.sub(nonblank, indentation + r'\1', self.partial(name, context)) - return render(template, context) + return self.render_template(template, context) + return func + + def section_tag_function(self, name, parse_tree_, template_, delims): + def func(context): + template = template_ + parse_tree = parse_tree_ + data = context.get(name) + if not data: + return '' + elif callable(data): + template = call(val=data, view=context, template=template) + parse_tree = self.parse_string_to_tree(template, context, delims) + data = [ data ] + elif type(data) not in [list, tuple]: + data = [ data ] + + parts = [] + for element in data: + context.context_list.insert(0, element) + parts.append(render_parse_tree(parse_tree, context, delims)) + del context.context_list[0] + + return ''.join(parts) return func + def parse_string_to_tree(self, template, view, delims=('{{', '}}')): + + template = Template(template) + + template.view = view + template.to_unicode = self.to_unicode + template.escape = self.escape + template.partial = self.partial + template.otag, template.ctag = delims + + template._compile_regexps() + + return template.parse_to_tree() + def parse_to_tree(self, index=0): """ Parse a template into a syntax tree. @@ -235,7 +231,7 @@ class Template(object): tmpl = e.template end_index = e.position - tag = sectionTag if captures['tag'] == '#' else inverseTag + tag = self.section_tag_function if captures['tag'] == '#' else inverseTag func = tag(name, bufr, tmpl, (self.otag, self.ctag)) elif captures['tag'] in ['{', '&']: @@ -258,8 +254,19 @@ class Template(object): return end_index + def render_template(self, template, view, delims=('{{', '}}')): + """ + Arguments: + + template: template string + view: context + + """ + parse_tree = self.parse_string_to_tree(template, view, delims) + return render_parse_tree(parse_tree, view, template) + def render(self, encoding=None): - result = render(self.template, self.view) + result = self.render_template(self.template, self.view) if encoding is not None: result = result.encode(encoding) -- cgit v1.2.1 From 373ba0163fe4ec0dbd933f8ef08154fbdb0d4719 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 08:41:44 -0800 Subject: RenderEngine now calls Template class correctly. --- pystache/renderengine.py | 76 +++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index f6172b7..8415f92 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -116,11 +116,13 @@ class RenderEngine(object): context: a Context instance. """ - self.context = context + _template = Template(template=template) - self._compile_regexps() + _template.to_unicode = self.literal + _template.escape = self.escape + _template.partial = self.load_partial - return self._render(template) + return _template.render_template(template=template, context=context) def _compile_regexps(self): """ @@ -333,14 +335,13 @@ def call(val, view, template=None): return unicode(val) -def render_parse_tree(parse_tree, view, template): +def render_parse_tree(parse_tree, context, template): """ Convert a parse-tree into a string. """ - get_string = lambda val: call(val, view, template) + get_string = lambda val: call(val, context, template) parts = map(get_string, parse_tree) - return ''.join(parts) def inverseTag(name, parsed, template, delims): @@ -361,17 +362,9 @@ class Template(object): tag_re = None otag, ctag = '{{', '}}' - def __init__(self, template=None, context={}, **kwargs): - from view import View - + def __init__(self, template=None): self.template = template - 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""" @@ -397,16 +390,31 @@ class Template(object): return context.partial(name) def escape_tag_function(self, name): - fetch = self.literal_tag_function(name) + get_literal = self.literal_tag_function(name) def func(context): - return self.escape(fetch(context)) + u = get_literal(context) + u = self.escape(u) + return u return func def literal_tag_function(self, name): def func(context): val = context.get(name) - template = call(val=val, view=context) - return self.to_unicode(self.render_template(template, context)) + + if callable(val): + # According to the spec: + # + # When used as the data value for an Interpolation tag, + # the lambda MUST be treatable as an arity 0 function, + # and invoked as such. The returned value MUST be + # rendered against the default delimiters, then + # interpolated in place of the lambda. + template = val() + val = self.render_template(template, context) + + u = self.to_unicode(val) + return u + return func def partial_tag_function(self, name, indentation=''): @@ -432,22 +440,23 @@ class Template(object): parts = [] for element in data: - context.context_list.insert(0, element) + context.push(element) parts.append(render_parse_tree(parse_tree, context, delims)) - del context.context_list[0] + context.pop() return ''.join(parts) return func - def parse_string_to_tree(self, template, view, delims=('{{', '}}')): + def parse_string_to_tree(self, template, delims=('{{', '}}')): template = Template(template) - template.view = view - template.to_unicode = self.to_unicode + template.otag = delims[0] + template.ctag = delims[1] + template.escape = self.escape template.partial = self.partial - template.otag, template.ctag = delims + template.to_unicode = self.to_unicode template._compile_regexps() @@ -550,22 +559,17 @@ class Template(object): return end_index - def render_template(self, template, view, delims=('{{', '}}')): + def render_template(self, template, context, delims=('{{', '}}')): """ Arguments: template: template string - view: context + context: a Context instance """ - parse_tree = self.parse_string_to_tree(template, view, delims) - return render_parse_tree(parse_tree, view, template) - - def render(self, encoding=None): - result = self.render_template(self.template, self.view) - if encoding is not None: - result = result.encode(encoding) - - return result + if not isinstance(template, basestring): + raise AssertionError("template: %s" % repr(template)) + parse_tree = self.parse_string_to_tree(template=template, delims=delims) + return render_parse_tree(parse_tree, context, template) -- cgit v1.2.1 From e29f92b814d2c913f3836e8765f88db74b287e20 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 08:56:53 -0800 Subject: Fixed test case: test__escape_preserves_unicode_subclasses in test_renderengine.py. --- pystache/renderengine.py | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 8415f92..541be8b 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -389,31 +389,52 @@ class Template(object): def partial(self, name, context=None): return context.partial(name) + def _get_string_value(self, context, tag_name): + """ + Get a value from the given context as a basestring instance. + + """ + val = context.get(tag_name) + + # We use "==" rather than "is" to compare integers, as using "is" + # relies on an implementation detail of CPython. The test about + # rendering zeroes failed while using PyPy when using "is". + # See issue #34: https://github.com/defunkt/pystache/issues/34 + if not val and val != 0: + if tag_name != '.': + return '' + val = self.context.top() + + if callable(val): + # According to the spec: + # + # When used as the data value for an Interpolation tag, + # the lambda MUST be treatable as an arity 0 function, + # and invoked as such. The returned value MUST be + # rendered against the default delimiters, then + # interpolated in place of the lambda. + template = val() + val = self.render_template(template, context) + + if not isinstance(val, basestring): + val = str(val) + + return val + + def escape_tag_function(self, name): get_literal = self.literal_tag_function(name) def func(context): - u = get_literal(context) - u = self.escape(u) - return u + s = self._get_string_value(context, name) + s = self.escape(s) + return s return func def literal_tag_function(self, name): def func(context): - val = context.get(name) - - if callable(val): - # According to the spec: - # - # When used as the data value for an Interpolation tag, - # the lambda MUST be treatable as an arity 0 function, - # and invoked as such. The returned value MUST be - # rendered against the default delimiters, then - # interpolated in place of the lambda. - template = val() - val = self.render_template(template, context) - - u = self.to_unicode(val) - return u + s = self._get_string_value(context, name) + s = self.to_unicode(s) + return s return func -- cgit v1.2.1 From 06d0b3e180609b496ee13963674f43db7df03e67 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 09:08:18 -0800 Subject: Got partials working. --- pystache/renderengine.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 541be8b..03c3c9f 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -120,7 +120,7 @@ class RenderEngine(object): _template.to_unicode = self.literal _template.escape = self.escape - _template.partial = self.load_partial + _template.get_partial = self.load_partial return _template.render_template(template=template, context=context) @@ -386,8 +386,12 @@ class Template(object): def escape(self, text): return cgi.escape(text, True) - def partial(self, name, context=None): - return context.partial(name) + def get_partial(self, name): + pass + + def _render_partial(self, name, context): + template = self.get_partial(name) + return self.render_template(template, context) def _get_string_value(self, context, tag_name): """ @@ -421,7 +425,6 @@ class Template(object): return val - def escape_tag_function(self, name): get_literal = self.literal_tag_function(name) def func(context): @@ -441,7 +444,7 @@ class Template(object): def partial_tag_function(self, name, indentation=''): def func(context): nonblank = re.compile(r'^(.)', re.M) - template = re.sub(nonblank, indentation + r'\1', self.partial(name, context)) + template = re.sub(nonblank, indentation + r'\1', self._render_partial(name, context)) return self.render_template(template, context) return func @@ -476,7 +479,7 @@ class Template(object): template.ctag = delims[1] template.escape = self.escape - template.partial = self.partial + template.get_partial = self.get_partial template.to_unicode = self.to_unicode template._compile_regexps() -- cgit v1.2.1 From 921a771d7c4320b42d5fded8357ca45ae279a6c1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 09:10:55 -0800 Subject: Fixed argument list in call to self.parse_string_to_tree(). --- pystache/renderengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 03c3c9f..d38c06c 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -457,7 +457,7 @@ class Template(object): return '' elif callable(data): template = call(val=data, view=context, template=template) - parse_tree = self.parse_string_to_tree(template, context, delims) + parse_tree = self.parse_string_to_tree(template, delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] -- cgit v1.2.1 From 7e65bb6df32e30f33244dd6b4cd4e24c1e445a4d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 09:34:36 -0800 Subject: Couple more tweaks: includes fixing issue rendering function-valued sections. --- pystache/renderengine.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index d38c06c..b3a9ff6 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -407,7 +407,7 @@ class Template(object): if not val and val != 0: if tag_name != '.': return '' - val = self.context.top() + val = context.top() if callable(val): # According to the spec: @@ -454,9 +454,10 @@ class Template(object): parse_tree = parse_tree_ data = context.get(name) if not data: - return '' + data = [] elif callable(data): - template = call(val=data, view=context, template=template) + # TODO: should we check the arity? + template = data(template) parse_tree = self.parse_string_to_tree(template, delims) data = [ data ] elif type(data) not in [list, tuple]: -- cgit v1.2.1 From 18d628611b981acf3c9f224e48c111a83dd1234c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 11:09:34 -0800 Subject: Fixed implicit iterators not working. --- pystache/renderengine.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index b3a9ff6..95d963d 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -366,7 +366,16 @@ class Template(object): self.template = template def _compile_regexps(self): - tags = {'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag)} + + # The possible tag type characters following the opening tag, + # excluding "=" and "{". + tag_types = "!>&/#^" + + # TODO: are we following this in the spec? + # + # The tag's content MUST be a non-whitespace character sequence + # NOT containing the current closing delimiter. + # tag = r""" (?P[\s\S]*?) (?P[\ \t]*) @@ -374,11 +383,12 @@ class Template(object): (?: (?P=) \s* (?P.+?) \s* = | (?P{) \s* (?P.+?) \s* } | - (?P\W?) \s* (?P[\s\S]+?) + (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) ) \s* %(ctag)s - """ - self.tag_re = re.compile(tag % tags, re.M | re.X) + """ % {'tag_types': tag_types, 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag)} + + self.tag_re = re.compile(tag, re.M | re.X) def to_unicode(self, text): return unicode(text) -- cgit v1.2.1 From 8025298fcd85b40c47d7b760a015d83bffaf8e86 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 11:22:44 -0800 Subject: Fixed partials to indent before rendering. --- pystache/renderengine.py | 8 +++----- tests/spec_cases.py | 6 +++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 95d963d..4e5bc3e 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -399,10 +399,6 @@ class Template(object): def get_partial(self, name): pass - def _render_partial(self, name, context): - template = self.get_partial(name) - return self.render_template(template, context) - def _get_string_value(self, context, tag_name): """ Get a value from the given context as a basestring instance. @@ -454,7 +450,9 @@ class Template(object): def partial_tag_function(self, name, indentation=''): def func(context): nonblank = re.compile(r'^(.)', re.M) - template = re.sub(nonblank, indentation + r'\1', self._render_partial(name, context)) + template = self.get_partial(name) + # Indent before rendering. + template = re.sub(nonblank, indentation + r'\1', template) return self.render_template(template, context) return func diff --git a/tests/spec_cases.py b/tests/spec_cases.py index 3d34e18..a379f24 100644 --- a/tests/spec_cases.py +++ b/tests/spec_cases.py @@ -55,7 +55,11 @@ def buildTest(testData, spec_filename): Template: \"""%s\""" Expected: %s - Actual: %s""" % (description, template, repr(expected), repr(actual)) + Actual: %s + + Expected: \"""%s\""" + Actual: \"""%s\""" + """ % (description, template, repr(expected), repr(actual), expected, actual) self.assertEquals(actual, expected, message) -- cgit v1.2.1 From 02d87ba6284a4ebaa4b5ad953e9c8f1be6b04b92 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 11:39:51 -0800 Subject: Fixed last spec-test: non-string template during rendering process. --- pystache/renderengine.py | 14 ++++++++++++-- tests/test_renderengine.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 4e5bc3e..2f246e2 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -116,6 +116,11 @@ class RenderEngine(object): context: a Context instance. """ + # Be strict but not too strict. In other words, accept str instead + # of unicode, but don't assume anything about the encoding (e.g. + # don't use self.literal). + template = unicode(template) + _template = Template(template=template) _template.to_unicode = self.literal @@ -424,6 +429,11 @@ class Template(object): # rendered against the default delimiters, then # interpolated in place of the lambda. template = val() + if not isinstance(template, basestring): + # In case the template is an integer, for example. + template = str(template) + if type(template) is not unicode: + template = self.to_unicode(template) val = self.render_template(template, context) if not isinstance(val, basestring): @@ -600,8 +610,8 @@ class Template(object): context: a Context instance """ - if not isinstance(template, basestring): - raise AssertionError("template: %s" % repr(template)) + if type(template) is not unicode: + raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) parse_tree = self.parse_string_to_tree(template=template, delims=delims) return render_parse_tree(parse_tree, context, template) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index d2f4397..f913828 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -68,7 +68,7 @@ class RenderEngineEnderTestCase(unittest.TestCase): """ engine = self._engine() - partials = {'partial': "{{person}}"} + partials = {'partial': u"{{person}}"} engine.load_partial = lambda key: partials[key] self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) -- cgit v1.2.1 From f143e1ed96c990e890ae8257c14d253c95d50fa8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 12:10:17 -0800 Subject: Adjusted README's testing instructions. --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 40ba330..e3974b8 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ nose_ works great! :: pip install nose cd pystache - nosetests --with-doctest --doctest-extension=rst + nosetests To include tests from the mustache spec_ in your test runs: :: @@ -73,6 +73,10 @@ To include tests from the mustache spec_ in your test runs: :: git submodule update nosetests -i spec +To run all available tests: + + nosetests --with-doctest --doctest-extension=rst -i spec + Mailing List ================== -- cgit v1.2.1 From d595856b5fb4ed7132ad8819b9b541f26872d92b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 12:25:51 -0800 Subject: READM view example is now more real. --- README.rst | 21 ++++++++++++--------- examples/readme.py | 3 +++ examples/say_hello.mustache | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 examples/readme.py create mode 100644 examples/say_hello.mustache diff --git a/README.rst b/README.rst index e3974b8..90e9510 100644 --- a/README.rst +++ b/README.rst @@ -41,21 +41,24 @@ Use It You can also create dedicated view classes to hold your view logic. -Here's your simple.py:: +Here's your view class (in `examples/readme.py`):: - >>> class Simple(object): - ... def thing(self): - ... return "pizza" + class SayHello(object): + def to(self): + return "World" -Then your template, simple.mustache:: + >>> from examples.readme import SayHello + >>> hello = SayHello() - Hi {{thing}}! +Then your template, `say_hello.mustache`:: + + Hello, {{to}}! Pull it together:: - >>> renderer = pystache.Renderer(search_dirs='examples') - >>> renderer.render(Simple()) - u'Hi pizza!' + >>> renderer = pystache.Renderer() + >>> renderer.render(hello) + u'Hello, World!' Test It diff --git a/examples/readme.py b/examples/readme.py new file mode 100644 index 0000000..8c6763f --- /dev/null +++ b/examples/readme.py @@ -0,0 +1,3 @@ +class SayHello(object): + def to(self): + return "World" diff --git a/examples/say_hello.mustache b/examples/say_hello.mustache new file mode 100644 index 0000000..7d8dfea --- /dev/null +++ b/examples/say_hello.mustache @@ -0,0 +1 @@ +Hello, {{to}}! \ No newline at end of file -- cgit v1.2.1 From 8b92c96fa9ac599359cc3a1997928af8c71d3172 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 12:34:23 -0800 Subject: README formatting tweaks. --- README.rst | 12 +++++++----- examples/readme.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 90e9510..97c0100 100644 --- a/README.rst +++ b/README.rst @@ -41,16 +41,18 @@ Use It You can also create dedicated view classes to hold your view logic. -Here's your view class (in `examples/readme.py`):: +Here's your view class (in ``examples/readme.py``):: class SayHello(object): def to(self): - return "World" + return "Pizza" + +Like so:: >>> from examples.readme import SayHello >>> hello = SayHello() -Then your template, `say_hello.mustache`:: +Then your template, ``say_hello.mustache``:: Hello, {{to}}! @@ -58,7 +60,7 @@ Pull it together:: >>> renderer = pystache.Renderer() >>> renderer.render(hello) - u'Hello, World!' + u'Hello, Pizza!' Test It @@ -76,7 +78,7 @@ To include tests from the mustache spec_ in your test runs: :: git submodule update nosetests -i spec -To run all available tests: +To run all available tests:: nosetests --with-doctest --doctest-extension=rst -i spec diff --git a/examples/readme.py b/examples/readme.py index 8c6763f..23b44f5 100644 --- a/examples/readme.py +++ b/examples/readme.py @@ -1,3 +1,3 @@ class SayHello(object): def to(self): - return "World" + return "Pizza" -- cgit v1.2.1 From ca419315a9a38c9ce16bb9f1f975d157f4e43052 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 13:06:46 -0800 Subject: Addressed issue #46: fixed test case re: interpolating lambda return values. --- tests/test_renderengine.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index f913828..3c2687c 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -243,12 +243,20 @@ class RenderEngineEnderTestCase(unittest.TestCase): def test_render__section__lambda__tag_in_output(self): """ - Check that callable output isn't treated as a template string (issue #46). + Check that callable output is treated as a template string (issue #46). + + The spec says-- + + 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. """ - template = '{{#test}}Mom{{/test}}' - context = {'test': (lambda text: '{{hi}} %s' % text)} - self._assert_render('{{hi}} Mom', template, context) + template = '{{#test}}Hi {{person}}{{/test}}' + context = {'person': 'Mom', 'test': (lambda text: text + " :)")} + self._assert_render('Hi Mom :)', template, context) def test_render__section__comment__multiline(self): """ -- cgit v1.2.1 From 78ca9df1d96ec5590703c8e6a1e1d7c3005039ae Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 13:40:21 -0800 Subject: Fixed spacing issues on partial test cases: trailing newline for standalones. --- examples/inner_partial.txt | 2 +- tests/common.py | 14 ++++++++++++++ tests/test_examples.py | 10 +++++----- tests/test_simple.py | 20 ++++++++++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/examples/inner_partial.txt b/examples/inner_partial.txt index 3d1396e..650c959 100644 --- a/examples/inner_partial.txt +++ b/examples/inner_partial.txt @@ -1 +1 @@ -## Again, {{title}}! ## +## Again, {{title}}! ## \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 0fa5e38..5d6cca6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -13,3 +13,17 @@ DATA_DIR = 'tests/data' def get_data_path(file_name): return os.path.join(DATA_DIR, file_name) + +def assert_strings(test_case, actual, expected): + # Show both friendly and literal versions. + message = """\ + + + Expected: \"""%s\""" + Actual: \"""%s\""" + + Expected: %s + Actual: %s""" % (expected, actual, repr(expected), repr(actual)) + test_case.assertEquals(actual, expected, message) + + diff --git a/tests/test_examples.py b/tests/test_examples.py index d28f8f5..0b523d6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -13,6 +13,9 @@ from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext +from tests.common import assert_strings + + class TestView(unittest.TestCase): def test_comments(self): self.assertEquals(Comments().render(), """

A Comedy of Errors

@@ -47,13 +50,10 @@ Again, Welcome!""") def test_template_partial_extension(self): view = TemplatePartial() view.template_extension = 'txt' - self.assertEquals(view.render(), """Welcome + assert_strings(self, view.render(), u"""Welcome ------- -## Again, Welcome! ## - -""") - +## Again, Welcome! ##""") def test_delimiters(self): self.assertEquals(Delimiters().render(), """ diff --git a/tests/test_simple.py b/tests/test_simple.py index cd3dec8..da85324 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,4 +1,5 @@ import unittest + import pystache from pystache import Renderer from examples.nested_context import NestedContext @@ -7,6 +8,9 @@ from examples.lambdas import Lambdas from examples.template_partial import TemplatePartial from examples.simple import Simple +from tests.common import assert_strings + + class TestSimple(unittest.TestCase): def test_nested_context(self): @@ -44,11 +48,19 @@ class TestSimple(unittest.TestCase): def test_template_partial_extension(self): + """ + Side note: + + From the spec-- + + Partial tags SHOULD be treated as standalone when appropriate. + + In particular, this means that trailing newlines should be removed. + + """ view = TemplatePartial() view.template_extension = 'txt' - self.assertEquals(view.render(), """Welcome + assert_strings(self, view.render(), u"""Welcome ------- -## Again, Welcome! ## - -""") +## Again, Welcome! ##""") -- cgit v1.2.1 From 4e04f6108aa940a3b1f675f9f247bd37ce1a6f89 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 13:44:03 -0800 Subject: Fixed spacing on final test case (standalone tags). --- tests/test_examples.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 0b523d6..7be3936 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -56,11 +56,8 @@ Again, Welcome!""") ## Again, Welcome! ##""") def test_delimiters(self): - self.assertEquals(Delimiters().render(), """ -* It worked the first time. - + assert_strings(self, Delimiters().render(), """* It worked the first time. * And it worked the second time. - * Then, surprisingly, it worked the third time. """) -- cgit v1.2.1 From ee3d4a65676453be2076efdd50ef3921a0d088bb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 14:32:45 -0800 Subject: Spec tests now included in nosetests by default. --- README.rst | 5 ++-- tests/spec_cases.py | 81 ----------------------------------------------------- tests/test_spec.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 84 deletions(-) delete mode 100644 tests/spec_cases.py create mode 100644 tests/test_spec.py diff --git a/README.rst b/README.rst index 97c0100..1626357 100644 --- a/README.rst +++ b/README.rst @@ -76,11 +76,10 @@ To include tests from the mustache spec_ in your test runs: :: git submodule init git submodule update - nosetests -i spec -To run all available tests:: +To run all available tests (including doctests):: - nosetests --with-doctest --doctest-extension=rst -i spec + nosetests --with-doctest --doctest-extension=rst Mailing List diff --git a/tests/spec_cases.py b/tests/spec_cases.py deleted file mode 100644 index a379f24..0000000 --- a/tests/spec_cases.py +++ /dev/null @@ -1,81 +0,0 @@ -# coding: utf-8 - -""" -Creates a unittest.TestCase for the tests defined in the mustache spec. - -We did not call this file something like "test_spec.py" to avoid matching -nosetests's default regular expression "(?:^|[\b_\./-])[Tt]est". -This allows us to exclude the spec test cases by default when running -nosetests. To include the spec tests, one can use the following option, -for example-- - - nosetests -i spec - -""" - -import glob -import os.path -import unittest -import yaml - -from pystache.renderer import Renderer - - -def code_constructor(loader, node): - value = loader.construct_mapping(node) - return eval(value['python'], {}) - -yaml.add_constructor(u'!code', code_constructor) - -specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') -specs = glob.glob(os.path.join(specs, '*.yml')) - -class MustacheSpec(unittest.TestCase): - pass - -def buildTest(testData, spec_filename): - - name = testData['name'] - description = testData['desc'] - - test_name = "%s (%s)" % (name, spec_filename) - - def test(self): - template = testData['template'] - partials = testData.has_key('partials') and testData['partials'] or {} - expected = testData['expected'] - data = testData['data'] - - renderer = Renderer(partials=partials) - actual = renderer.render(template, data) - actual = actual.encode('utf-8') - - message = """%s - - Template: \"""%s\""" - - Expected: %s - Actual: %s - - Expected: \"""%s\""" - Actual: \"""%s\""" - """ % (description, template, repr(expected), repr(actual), expected, actual) - - self.assertEquals(actual, expected, message) - - # The name must begin with "test" for nosetests test discovery to work. - test.__name__ = 'test: "%s"' % test_name - - return test - -for spec in specs: - file_name = os.path.basename(spec) - - for test in yaml.load(open(spec))['tests']: - test = buildTest(test, file_name) - setattr(MustacheSpec, test.__name__, test) - # Prevent this variable from being interpreted as another test. - del(test) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_spec.py b/tests/test_spec.py new file mode 100644 index 0000000..a379f24 --- /dev/null +++ b/tests/test_spec.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +""" +Creates a unittest.TestCase for the tests defined in the mustache spec. + +We did not call this file something like "test_spec.py" to avoid matching +nosetests's default regular expression "(?:^|[\b_\./-])[Tt]est". +This allows us to exclude the spec test cases by default when running +nosetests. To include the spec tests, one can use the following option, +for example-- + + nosetests -i spec + +""" + +import glob +import os.path +import unittest +import yaml + +from pystache.renderer import Renderer + + +def code_constructor(loader, node): + value = loader.construct_mapping(node) + return eval(value['python'], {}) + +yaml.add_constructor(u'!code', code_constructor) + +specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') +specs = glob.glob(os.path.join(specs, '*.yml')) + +class MustacheSpec(unittest.TestCase): + pass + +def buildTest(testData, spec_filename): + + name = testData['name'] + description = testData['desc'] + + test_name = "%s (%s)" % (name, spec_filename) + + def test(self): + template = testData['template'] + partials = testData.has_key('partials') and testData['partials'] or {} + expected = testData['expected'] + data = testData['data'] + + renderer = Renderer(partials=partials) + actual = renderer.render(template, data) + actual = actual.encode('utf-8') + + message = """%s + + Template: \"""%s\""" + + Expected: %s + Actual: %s + + Expected: \"""%s\""" + Actual: \"""%s\""" + """ % (description, template, repr(expected), repr(actual), expected, actual) + + self.assertEquals(actual, expected, message) + + # The name must begin with "test" for nosetests test discovery to work. + test.__name__ = 'test: "%s"' % test_name + + return test + +for spec in specs: + file_name = os.path.basename(spec) + + for test in yaml.load(open(spec))['tests']: + test = buildTest(test, file_name) + setattr(MustacheSpec, test.__name__, test) + # Prevent this variable from being interpreted as another test. + del(test) + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1 From aa1a256dd0ab619492647b4e74d246f74ef0390f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 17:47:13 -0800 Subject: Spacing; added a link to the spec; and made the author line a doctest. --- README.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 1626357..04cacbf 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,9 @@ Logo: David Phillips - http://davidphillips.us/ Documentation ============= -The different Mustache tags are documented at `mustache(5)`_. +The `mustache(5)`_ man page is the main entry point for understanding +Mustache syntax. Beyond this, the Mustache spec_ provides more complete +(and more current) documentation of Mustache's behavior. Install It ========== @@ -41,9 +43,10 @@ Use It You can also create dedicated view classes to hold your view logic. -Here's your view class (in ``examples/readme.py``):: +Here's your view class (in examples/readme.py):: class SayHello(object): + def to(self): return "Pizza" @@ -52,7 +55,7 @@ Like so:: >>> from examples.readme import SayHello >>> hello = SayHello() -Then your template, ``say_hello.mustache``:: +Then your template, say_hello.mustache:: Hello, {{to}}! @@ -72,7 +75,7 @@ nose_ works great! :: cd pystache nosetests -To include tests from the mustache spec_ in your test runs: :: +To include tests from the Mustache spec_ in your test runs: :: git submodule init git submodule update @@ -84,7 +87,8 @@ To run all available tests (including doctests):: Mailing List ================== -As of Nov 26, 2011, there's a mailing list, pystache@librelist.com. + +As of November 2011, there's a mailing list, pystache@librelist.com. Archive: http://librelist.com/browser/pystache/ @@ -96,8 +100,9 @@ Author :: - context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' } - pystache.render("{{author}} :: {{email}}", context) + >>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' } + >>> pystache.render("{{author}} :: {{email}}", context) + u'Chris Wanstrath :: chris@ozmm.org' .. _ctemplate: http://code.google.com/p/google-ctemplate/ -- cgit v1.2.1 From de5451d4912fb67f5e23ff00adc4770ebfd9bd7d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 17:53:19 -0800 Subject: Tweaked leading sentence of README. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 04cacbf..801c653 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ Pystache .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png -Inspired by ctemplate_ and et_, Mustache_ is a -framework-agnostic way to render logic-free views. +Mustache_ is a framework-agnostic way to render logic-free views that is +inspired by ctemplate_ and et_. As ctemplates says, "It emphasizes separating logic from presentation: it is impossible to embed application logic in this template language." -- cgit v1.2.1 From 2a3d41583ae746740ef26f304e14e6b68f1ea491 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 18:17:23 -0800 Subject: Added a test case for issue #20: "custom delimiter in sections not working" --- tests/test_renderengine.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 3c2687c..ba2c924 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -10,6 +10,7 @@ import unittest from pystache.context import Context from pystache.renderengine import RenderEngine +from tests.common import assert_strings class RenderEngineTestCase(unittest.TestCase): @@ -29,9 +30,16 @@ class RenderEngineTestCase(unittest.TestCase): self.assertEquals(engine.load_partial, "foo") -class RenderEngineEnderTestCase(unittest.TestCase): +class RenderTests(unittest.TestCase): - """Test RenderEngine.render().""" + """ + Tests RenderEngine.render(). + + Explicit spec-test-like tests best go in this class since the + RenderEngine class contains all parsing logic. This way, the unit tests + will be more focused and fail "closer to the code". + + """ def _engine(self): """ @@ -57,7 +65,7 @@ class RenderEngineEnderTestCase(unittest.TestCase): actual = engine.render(template, context) - self.assertEquals(actual, expected) + assert_strings(test_case=self, actual=actual, expected=expected) def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) @@ -266,3 +274,14 @@ class RenderEngineEnderTestCase(unittest.TestCase): self._assert_render('foobar', 'foo{{! baz }}bar') self._assert_render('foobar', 'foo{{! \nbaz }}bar') + def test_custom_delimiters__sections(self): + """ + Check that custom delimiters can be used to start a section. + + Test case for issue #20: https://github.com/defunkt/pystache/issues/20 + + """ + template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]' + context = {'foo': True} + self._assert_render("bar", template, context) + -- cgit v1.2.1 From 1a9a2194b8d589432c135ebc485eb1c301db0923 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 18:33:38 -0800 Subject: Added (succeeding) test case for issue #24: "nested truthy" --- tests/test_renderengine.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index ba2c924..3be4788 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -283,5 +283,18 @@ class RenderTests(unittest.TestCase): """ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]' context = {'foo': True} - self._assert_render("bar", template, context) + self._assert_render(u"bar", template, context) + def test_sections__nested_truthy(self): + """ + Check that "nested truthy" sections get rendered. + + Test case for issue #24: https://github.com/defunkt/pystache/issues/24 + + This test is copied from the spec. We explicitly include it to + prevent regressions for those who don't pull down the spec tests. + + """ + template = "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" + context = {'bool': True} + self._assert_render(u"| A B C D E |", template, context) -- cgit v1.2.1 From ef396e5dc1bb3eaa30b24e85ce319ac29d367042 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 19:04:43 -0800 Subject: Added test case for issue #35: "Changing delimiters back is retroactive" --- tests/test_renderengine.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 3be4788..9dd7898 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -285,6 +285,17 @@ class RenderTests(unittest.TestCase): context = {'foo': True} self._assert_render(u"bar", template, context) + def test_custom_delimiters__not_retroactive(self): + """ + Check that changing custom delimiters back is not "retroactive." + + Test case for issue #35: https://github.com/defunkt/pystache/issues/35 + + """ + expected = u' {{foo}} ' + self._assert_render(expected, '{{=$ $=}} {{foo}} ') + self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. + def test_sections__nested_truthy(self): """ Check that "nested truthy" sections get rendered. -- cgit v1.2.1 From 469c49ca6d34a6966aa13c105e856b15cd47e844 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 19:19:38 -0800 Subject: Added (succeeding) test case for issue #36. --- tests/test_renderengine.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 9dd7898..841b4d9 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -283,7 +283,7 @@ class RenderTests(unittest.TestCase): """ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]' context = {'foo': True} - self._assert_render(u"bar", template, context) + self._assert_render(u'bar', template, context) def test_custom_delimiters__not_retroactive(self): """ @@ -306,6 +306,22 @@ class RenderTests(unittest.TestCase): prevent regressions for those who don't pull down the spec tests. """ - template = "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" + template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |' context = {'bool': True} - self._assert_render(u"| A B C D E |", template, context) + self._assert_render(u'| A B C D E |', template, context) + + def test_sections__nested_with_same_keys(self): + """ + Check a doubly-nested section with the same context key. + + Test case for issue #36: https://github.com/defunkt/pystache/issues/36 + + """ + # Start with an easier, working case. + template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}' + context = {'x': {'z': {'y': 1}}} + self._assert_render(u'1', template, context) + + template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}' + context = {'x': {'x': {'y': 1}}} + self._assert_render(u'1', template, context) -- cgit v1.2.1 From 594bc73bc91886994f617e506af3263969f51fc3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 19:43:11 -0800 Subject: Addressed issue #43: added spec version that pystache complies with. --- README.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 801c653..3292fc8 100644 --- a/README.rst +++ b/README.rst @@ -10,13 +10,18 @@ inspired by ctemplate_ and et_. 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. +Pystache is a Python implementation of Mustache. Currently, it passes +all tests in version 1.0.3_ of the Mustache spec_. Pystache is semantically versioned: http://semver.org. Logo: David Phillips - http://davidphillips.us/ +Requirements +============ + +Pystache is currently tested under Python 2.6. + Documentation ============= @@ -111,3 +116,4 @@ Author .. _mustache(5): http://mustache.github.com/mustache.5.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html .. _spec: https://github.com/mustache/spec +.. _v1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From 52349a8b1d0e6e3c2c6277521ad4f1aa362ab498 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 19:47:48 -0800 Subject: Fixed typo in README to fix spec link. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3292fc8..02fca8f 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ 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. Currently, it passes -all tests in version 1.0.3_ of the Mustache spec_. +all tests in version v1.0.3_ of the Mustache spec_. Pystache is semantically versioned: http://semver.org. -- cgit v1.2.1 From 8f2ef63a255372b27d28d5a597942d1d811d7878 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 20:05:20 -0800 Subject: Deleted the Modifiers class, which is no longer used. --- pystache/renderengine.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 2f246e2..5430c8f 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -22,30 +22,6 @@ except ImportError: return hasattr(it, '__call__') -class Modifiers(dict): - - """Dictionary with a decorator for assigning functions to keys.""" - - def set(self, key): - """ - Return a decorator that assigns the given function to the given key. - - >>> modifiers = Modifiers() - >>> - >>> @modifiers.set('P') - ... def render_tongue(tag_name, context): - ... return "%s :P" % context.get(tag_name) - >>> - >>> modifiers['P']('text', {'text': 'hello!'}) - 'hello! :P' - - """ - def decorate(func): - self[key] = func - return func - return decorate - - class RenderEngine(object): """ @@ -68,8 +44,6 @@ class RenderEngine(object): otag = '{{' ctag = '}}' - modifiers = Modifiers() - def __init__(self, load_partial=None, literal=None, escape=None): """ Arguments: @@ -210,7 +184,6 @@ class RenderEngine(object): return val - @modifiers.set(None) def _render_escaped(self, tag_name): """ Return a variable value as an escaped unicode string. @@ -219,8 +192,6 @@ class RenderEngine(object): s = self._get_string_context(tag_name) return self.escape(s) - @modifiers.set('{') - @modifiers.set('&') def _render_literal(self, tag_name): """ Return a variable value as a unicode string (unescaped). @@ -229,16 +200,13 @@ class RenderEngine(object): s = self._get_string_context(tag_name) return self.literal(s) - @modifiers.set('!') def _render_comment(self, tag_name): return '' - @modifiers.set('>') def _render_partial(self, template_name): template = self.load_partial(template_name) return self._render(template) - @modifiers.set('=') def _change_delimiter(self, tag_name): """ Change the current delimiter. -- cgit v1.2.1 From 27af8b1528739117cb3c767b2ecf9dc97b73da9d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 20:06:51 -0800 Subject: Moved the END_OF_LINE_CHARACTERS constant to the top of the module. --- pystache/renderengine.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 5430c8f..72bbd14 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -12,6 +12,9 @@ import re import types +END_OF_LINE_CHARACTERS = ['\r', '\n'] + + try: # The collections.Callable class is not available until Python 2.6. import collections.Callable @@ -272,11 +275,6 @@ class RenderEngine(object): output = "".join(output) return output -# - - -END_OF_LINE_CHARACTERS = ['\r', '\n'] - # TODO: what are the possibilities for val? def call(val, view, template=None): -- cgit v1.2.1 From 1979c2bb8981ef51a1d9df5f3522146a9b43a8f7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 21:56:51 -0800 Subject: Moved functions to top of renderengine.py. --- pystache/renderengine.py | 110 ++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 72bbd14..d34b974 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -25,6 +25,63 @@ except ImportError: return hasattr(it, '__call__') +# TODO: what are the possibilities for val? +def call(val, view, template=None): + if callable(val): + (args, _, _, _) = inspect.getargspec(val) + + args_count = len(args) + + if not isinstance(val, types.FunctionType): + # Then val is an instance method. Subtract one from the + # argument count because Python will automatically prepend + # self to the argument list when calling. + args_count -=1 + + if args_count is 0: + val = val() + elif args_count is 1 and args[0] in ['self', 'context']: + val = val(view) + elif args_count is 1: + val = val(template) + else: + val = val(view, template) + + if callable(val): + val = val(template) + + if val is None: + val = '' + + return unicode(val) + + +def render_parse_tree(parse_tree, context, template): + """ + Convert a parse-tree into a string. + + """ + get_string = lambda val: call(val, context, template) + parts = map(get_string, parse_tree) + return ''.join(parts) + + +def inverseTag(name, parsed, template, delims): + def func(self): + data = self.get(name) + if data: + return '' + return render_parse_tree(parsed, self, delims) + return func + + +class EndOfSection(Exception): + def __init__(self, parse_tree, template, position): + self.parse_tree = parse_tree + self.template = template + self.position = position + + class RenderEngine(object): """ @@ -276,59 +333,6 @@ class RenderEngine(object): return output -# TODO: what are the possibilities for val? -def call(val, view, template=None): - if callable(val): - (args, _, _, _) = inspect.getargspec(val) - - args_count = len(args) - - if not isinstance(val, types.FunctionType): - # Then val is an instance method. Subtract one from the - # argument count because Python will automatically prepend - # self to the argument list when calling. - args_count -=1 - - if args_count is 0: - val = val() - elif args_count is 1 and args[0] in ['self', 'context']: - val = val(view) - elif args_count is 1: - val = val(template) - else: - val = val(view, template) - - if callable(val): - val = val(template) - - if val is None: - val = '' - - return unicode(val) - -def render_parse_tree(parse_tree, context, template): - """ - Convert a parse-tree into a string. - - """ - get_string = lambda val: call(val, context, template) - parts = map(get_string, parse_tree) - return ''.join(parts) - -def inverseTag(name, parsed, template, delims): - def func(self): - data = self.get(name) - if data: - return '' - return render_parse_tree(parsed, self, delims) - return func - -class EndOfSection(Exception): - def __init__(self, parse_tree, template, position): - self.parse_tree = parse_tree - self.template = template - self.position = position - class Template(object): tag_re = None otag, ctag = '{{', '}}' -- cgit v1.2.1 From 47f24b4ed741a33d62340ba14da3c14d76412c6d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 21:58:41 -0800 Subject: Deleted no-longer-used methods from RenderEngine. --- pystache/renderengine.py | 169 ----------------------------------------------- 1 file changed, 169 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index d34b974..0379afd 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -163,175 +163,6 @@ class RenderEngine(object): return _template.render_template(template=template, context=context) - def _compile_regexps(self): - """ - Compile and set the regular expression attributes. - - This method uses the current values for the otag and ctag attributes. - - """ - tags = { - 'otag': re.escape(self.otag), - 'ctag': re.escape(self.ctag) - } - - # The section contents include white space to comply with the spec's - # requirement that sections not alter surrounding whitespace. - section = r"%(otag)s([#|^])([^\}]*)%(ctag)s(.+?)%(otag)s/\2%(ctag)s" % tags - self.section_re = re.compile(section, re.M|re.S) - - tag = r"%(otag)s(#|=|&|!|>|\{)?(.+?)\1?%(ctag)s+" % tags - # We use re.DOTALL to permit multiline comments, in accordance with the spec. - self.tag_re = re.compile(tag, re.DOTALL) - - def _render_tags(self, template): - output = [] - - while True: - parts = self.tag_re.split(template, maxsplit=1) - output.append(parts[0]) - - if len(parts) < 2: - # Then there was no match. - break - - tag_type, tag_name, template = parts[1:] - - tag_name = tag_name.strip() - func = self.modifiers[tag_type] - tag_value = func(self, tag_name) - - # Appending the tag value to the output prevents treating the - # value as a template string (bug: issue #44). - output.append(tag_value) - - output = "".join(output) - - return output - - def _render_dictionary(self, template, context): - self.context.push(context) - out = self._render(template) - self.context.pop() - - return out - - def _render_list(self, template, listing): - insides = [] - for item in listing: - insides.append(self._render_dictionary(template, item)) - - return ''.join(insides) - - def _get_string_context(self, tag_name): - """ - Get a value from the current context as a basestring instance. - - """ - val = self.context.get(tag_name) - - # We use "==" rather than "is" to compare integers, as using "is" - # relies on an implementation detail of CPython. The test about - # rendering zeroes failed while using PyPy when using "is". - # See issue #34: https://github.com/defunkt/pystache/issues/34 - if not val and val != 0: - if tag_name != '.': - return '' - val = self.context.top() - - if not isinstance(val, basestring): - val = str(val) - - return val - - def _render_escaped(self, tag_name): - """ - Return a variable value as an escaped unicode string. - - """ - s = self._get_string_context(tag_name) - return self.escape(s) - - def _render_literal(self, tag_name): - """ - Return a variable value as a unicode string (unescaped). - - """ - s = self._get_string_context(tag_name) - return self.literal(s) - - def _render_comment(self, tag_name): - return '' - - def _render_partial(self, template_name): - template = self.load_partial(template_name) - return self._render(template) - - def _change_delimiter(self, tag_name): - """ - Change the current delimiter. - - """ - self.otag, self.ctag = tag_name.split(' ') - self._compile_regexps() - - return '' - - def _render(self, template): - """ - Arguments: - - template: a template string with type unicode. - - """ - output = [] - - while True: - parts = self.section_re.split(template, maxsplit=1) - - start = self._render_tags(parts[0]) - output.append(start) - - if len(parts) < 2: - # Then there was no match. - break - - section_type, section_key, section_contents, template = parts[1:] - - section_key = section_key.strip() - section_value = self.context.get(section_key, None) - - rendered = '' - - # Callable - if section_value and check_callable(section_value): - rendered = section_value(section_contents) - - # Dictionary - elif section_value and hasattr(section_value, 'keys') and hasattr(section_value, '__getitem__'): - if section_type != '^': - rendered = self._render_dictionary(section_contents, section_value) - - # Lists - elif section_value and hasattr(section_value, '__iter__'): - if section_type != '^': - rendered = self._render_list(section_contents, section_value) - - # Other objects - elif section_value and isinstance(section_value, object): - if section_type != '^': - rendered = self._render_dictionary(section_contents, section_value) - - # Falsey and Negated or Truthy and Not Negated - elif (not section_value and section_type == '^') or (section_value and section_type != '^'): - rendered = self._render_dictionary(section_contents, section_value) - - # Render template prior to section too - output.append(rendered) - - output = "".join(output) - return output - class Template(object): tag_re = None -- cgit v1.2.1 From c7007a708c286259550903290763cc02483c9d19 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 22:06:24 -0800 Subject: The Template class no longer has a template attribute. --- pystache/renderengine.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 0379afd..149fcd3 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -155,7 +155,7 @@ class RenderEngine(object): # don't use self.literal). template = unicode(template) - _template = Template(template=template) + _template = Template() _template.to_unicode = self.literal _template.escape = self.escape @@ -168,9 +168,6 @@ class Template(object): tag_re = None otag, ctag = '{{', '}}' - def __init__(self, template=None): - self.template = template - def _compile_regexps(self): # The possible tag type characters following the opening tag, @@ -277,7 +274,7 @@ class Template(object): elif callable(data): # TODO: should we check the arity? template = data(template) - parse_tree = self.parse_string_to_tree(template, delims) + parse_tree = self.parse_string_to_tree(template_string=template, delims=delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -291,9 +288,9 @@ class Template(object): return ''.join(parts) return func - def parse_string_to_tree(self, template, delims=('{{', '}}')): + def parse_string_to_tree(self, template_string, delims=('{{', '}}')): - template = Template(template) + template = Template() template.otag = delims[0] template.ctag = delims[1] @@ -304,15 +301,14 @@ class Template(object): template._compile_regexps() - return template.parse_to_tree() + return template.parse_to_tree(template=template_string) - def parse_to_tree(self, index=0): + def parse_to_tree(self, template, index=0): """ Parse a template into a syntax tree. """ parse_tree = [] - template = self.template start_index = index while True: @@ -325,15 +321,14 @@ class Template(object): match_index = match.end('content') end_index = match.end() - index = self._handle_match(parse_tree, captures, start_index, match_index, end_index) + index = self._handle_match(template, parse_tree, captures, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) return parse_tree - def _handle_match(self, parse_tree, captures, start_index, match_index, end_index): - template = self.template + def _handle_match(self, template, parse_tree, captures, start_index, match_index, end_index): # Normalize the captures dictionary. if captures['change'] is not None: @@ -374,7 +369,7 @@ class Template(object): elif captures['tag'] in ['#', '^']: try: - self.parse_to_tree(index=end_index) + self.parse_to_tree(template=template, index=end_index) except EndOfSection as e: bufr = e.parse_tree tmpl = e.template @@ -414,6 +409,6 @@ class Template(object): if type(template) is not unicode: raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - parse_tree = self.parse_string_to_tree(template=template, delims=delims) + parse_tree = self.parse_string_to_tree(template_string=template, delims=delims) return render_parse_tree(parse_tree, context, template) -- cgit v1.2.1 From 7826ec7f20349bb54f34f096bb86cebb08d5f1c1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 22:14:37 -0800 Subject: Merged Template class into RenderEngine. --- pystache/renderengine.py | 44 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 149fcd3..79f1afc 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -100,7 +100,9 @@ class RenderEngine(object): unicode subclasses (e.g. markupsafe.Markup). """ + tag_re = None + otag = '{{' ctag = '}}' @@ -155,18 +157,7 @@ class RenderEngine(object): # don't use self.literal). template = unicode(template) - _template = Template() - - _template.to_unicode = self.literal - _template.escape = self.escape - _template.get_partial = self.load_partial - - return _template.render_template(template=template, context=context) - - -class Template(object): - tag_re = None - otag, ctag = '{{', '}}' + return self.render_template(template=template, context=context) def _compile_regexps(self): @@ -193,15 +184,6 @@ class Template(object): self.tag_re = re.compile(tag, re.M | re.X) - def to_unicode(self, text): - return unicode(text) - - def escape(self, text): - return cgi.escape(text, True) - - def get_partial(self, name): - pass - def _get_string_value(self, context, tag_name): """ Get a value from the given context as a basestring instance. @@ -231,7 +213,7 @@ class Template(object): # In case the template is an integer, for example. template = str(template) if type(template) is not unicode: - template = self.to_unicode(template) + template = self.literal(template) val = self.render_template(template, context) if not isinstance(val, basestring): @@ -250,7 +232,7 @@ class Template(object): def literal_tag_function(self, name): def func(context): s = self._get_string_value(context, name) - s = self.to_unicode(s) + s = self.literal(s) return s return func @@ -258,7 +240,7 @@ class Template(object): def partial_tag_function(self, name, indentation=''): def func(context): nonblank = re.compile(r'^(.)', re.M) - template = self.get_partial(name) + template = self.load_partial(name) # Indent before rendering. template = re.sub(nonblank, indentation + r'\1', template) return self.render_template(template, context) @@ -290,18 +272,14 @@ class Template(object): def parse_string_to_tree(self, template_string, delims=('{{', '}}')): - template = Template() - - template.otag = delims[0] - template.ctag = delims[1] + engine = RenderEngine(load_partial=self.load_partial, literal=self.literal, escape=self.escape) - template.escape = self.escape - template.get_partial = self.get_partial - template.to_unicode = self.to_unicode + engine.otag = delims[0] + engine.ctag = delims[1] - template._compile_regexps() + engine._compile_regexps() - return template.parse_to_tree(template=template_string) + return engine.parse_to_tree(template=template_string) def parse_to_tree(self, template, index=0): """ -- cgit v1.2.1 From 8ff50e0bbccca6355f744beed3e748bf2c0f728a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 22:22:31 -0800 Subject: Removed the "delims" argument from a RenderEngine method. --- pystache/renderengine.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 79f1afc..6fd14a0 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -12,6 +12,8 @@ import re import types +DEFAULT_TAG_OPENING = '{{' +DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] @@ -103,8 +105,8 @@ class RenderEngine(object): tag_re = None - otag = '{{' - ctag = '}}' + otag = DEFAULT_TAG_OPENING + ctag = DEFAULT_TAG_CLOSING def __init__(self, load_partial=None, literal=None, escape=None): """ @@ -270,12 +272,13 @@ class RenderEngine(object): return ''.join(parts) return func - def parse_string_to_tree(self, template_string, delims=('{{', '}}')): + def parse_string_to_tree(self, template_string, delims=None): engine = RenderEngine(load_partial=self.load_partial, literal=self.literal, escape=self.escape) - engine.otag = delims[0] - engine.ctag = delims[1] + if delims is not None: + engine.otag = delims[0] + engine.ctag = delims[1] engine._compile_regexps() @@ -376,7 +379,7 @@ class RenderEngine(object): return end_index - def render_template(self, template, context, delims=('{{', '}}')): + def render_template(self, template, context): """ Arguments: @@ -387,6 +390,6 @@ class RenderEngine(object): if type(template) is not unicode: raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - parse_tree = self.parse_string_to_tree(template_string=template, delims=delims) + parse_tree = self.parse_string_to_tree(template_string=template) return render_parse_tree(parse_tree, context, template) -- cgit v1.2.1 From 249074576b9a2ef38d473342b6d4117d332499fa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 22:28:56 -0800 Subject: Changed the order of a couple methods in RenderEngine. --- pystache/renderengine.py | 60 +++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 6fd14a0..e99a5d2 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -159,7 +159,35 @@ class RenderEngine(object): # don't use self.literal). template = unicode(template) - return self.render_template(template=template, context=context) + return self._render_template(template=template, context=context) + + def _render_template(self, template, context): + """ + Arguments: + + template: template string + context: a Context instance + + """ + if type(template) is not unicode: + raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) + + parse_tree = self.parse_string_to_tree(template_string=template) + return render_parse_tree(parse_tree, context, template) + + def parse_string_to_tree(self, template_string, delims=None): + + engine = RenderEngine(load_partial=self.load_partial, + literal=self.literal, + escape=self.escape) + + if delims is not None: + engine.otag = delims[0] + engine.ctag = delims[1] + + engine._compile_regexps() + + return engine.parse_to_tree(template=template_string) def _compile_regexps(self): @@ -216,7 +244,7 @@ class RenderEngine(object): template = str(template) if type(template) is not unicode: template = self.literal(template) - val = self.render_template(template, context) + val = self._render_template(template, context) if not isinstance(val, basestring): val = str(val) @@ -245,7 +273,7 @@ class RenderEngine(object): template = self.load_partial(name) # Indent before rendering. template = re.sub(nonblank, indentation + r'\1', template) - return self.render_template(template, context) + return self._render_template(template, context) return func def section_tag_function(self, name, parse_tree_, template_, delims): @@ -272,18 +300,6 @@ class RenderEngine(object): return ''.join(parts) return func - def parse_string_to_tree(self, template_string, delims=None): - - engine = RenderEngine(load_partial=self.load_partial, literal=self.literal, escape=self.escape) - - if delims is not None: - engine.otag = delims[0] - engine.ctag = delims[1] - - engine._compile_regexps() - - return engine.parse_to_tree(template=template_string) - def parse_to_tree(self, template, index=0): """ Parse a template into a syntax tree. @@ -379,17 +395,3 @@ class RenderEngine(object): return end_index - def render_template(self, template, context): - """ - Arguments: - - template: template string - context: a Context instance - - """ - if type(template) is not unicode: - raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - - parse_tree = self.parse_string_to_tree(template_string=template) - return render_parse_tree(parse_tree, context, template) - -- cgit v1.2.1 From 7b1cb13218f0bb6247b1342e800a1362e20d15dc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 22:29:22 -0800 Subject: Removed callable try-except block at beginning of renderengine.py. --- pystache/renderengine.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index e99a5d2..884ce33 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -17,16 +17,6 @@ DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] -try: - # The collections.Callable class is not available until Python 2.6. - import collections.Callable - def check_callable(it): - return isinstance(it, collections.Callable) -except ImportError: - def check_callable(it): - return hasattr(it, '__call__') - - # TODO: what are the possibilities for val? def call(val, view, template=None): if callable(val): -- cgit v1.2.1 From 8189c762d8808bd93de286791e252d8c62b9a591 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 23:13:23 -0800 Subject: README tweaks: moved documentation into intro; cleaned up links. --- README.rst | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 02fca8f..bbacd7a 100644 --- a/README.rst +++ b/README.rst @@ -5,30 +5,25 @@ Pystache .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png Mustache_ is a framework-agnostic way to render logic-free views that is -inspired by ctemplate_ and et_. +inspired by ctemplate_ and et_. Like ctemplate_, "it emphasizes +separating logic from presentation: it is impossible to embed application +logic in this template language." -As ctemplates says, "It emphasizes separating logic from presentation: -it is impossible to embed application logic in this template language." +The `mustache(5)`_ man page provides a good introduction to Mustache's +syntax. For a more complete (and more current) description of Mustache's +behavior, see the official Mustache spec_. -Pystache is a Python implementation of Mustache. Currently, it passes -all tests in version v1.0.3_ of the Mustache spec_. +Pystache_ is a Python implementation of Mustache. It currently passes +all tests in `version 1.0.3`_ of the Mustache spec_. Pystache itself is +`semantically versioned`_. -Pystache is semantically versioned: http://semver.org. - -Logo: David Phillips - http://davidphillips.us/ +Logo: `David Phillips`_ Requirements ============ Pystache is currently tested under Python 2.6. -Documentation -============= - -The `mustache(5)`_ man page is the main entry point for understanding -Mustache syntax. Beyond this, the Mustache spec_ provides more complete -(and more current) documentation of Mustache's behavior. - Install It ========== @@ -111,9 +106,12 @@ Author .. _ctemplate: http://code.google.com/p/google-ctemplate/ +.. _David Phillips: http://davidphillips.us/ .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html -.. _Mustache: http://defunkt.github.com/mustache/ +.. _Mustache: http://mustache.github.com/ .. _mustache(5): http://mustache.github.com/mustache.5.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html +.. _Pystache: https://github.com/defunkt/pystache +.. _semantically versioned: http://semver.org .. _spec: https://github.com/mustache/spec -.. _v1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 +.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From 69490716fd93fa0947ac298c68e430c918b67c4d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Jan 2012 23:51:17 -0800 Subject: Added call() docstring. --- pystache/renderengine.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 884ce33..4587390 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -17,8 +17,22 @@ DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] -# TODO: what are the possibilities for val? def call(val, view, template=None): + """ + Arguments: + + val: the argument val can be any of the following: + + * a unicode string + * the return value of a call to any of the following: + + * RenderEngine.partial_tag_function() + * RenderEngine.section_tag_function() + * inverseTag() + * RenderEngine.literal_tag_function() + * RenderEngine.escape_tag_function() + + """ if callable(val): (args, _, _, _) = inspect.getargspec(val) -- cgit v1.2.1 From 3de74535d7af79c57bfa78e5e6c02e5384fc3857 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 00:40:55 -0800 Subject: Simplified calls to render_parse_tree() (removed the third argument). --- pystache/renderengine.py | 78 +++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 4587390..91c9ec0 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -17,7 +17,7 @@ DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] -def call(val, view, template=None): +def call(val, view): """ Arguments: @@ -26,11 +26,16 @@ def call(val, view, template=None): * a unicode string * the return value of a call to any of the following: - * RenderEngine.partial_tag_function() - * RenderEngine.section_tag_function() - * inverseTag() - * RenderEngine.literal_tag_function() - * RenderEngine.escape_tag_function() + * RenderEngine._make_get_literal(): + Args: context + * RenderEngine._make_get_escaped(): + Args: context + * RenderEngine._make_get_partial() + Args: context + * RenderEngine._make_get_section() + Args: context + * _make_get_inverse() + Args: context """ if callable(val): @@ -62,23 +67,24 @@ def call(val, view, template=None): return unicode(val) -def render_parse_tree(parse_tree, context, template): +def render_parse_tree(parse_tree, context): """ Convert a parse-tree into a string. """ - get_string = lambda val: call(val, context, template) + get_string = lambda val: call(val, context) parts = map(get_string, parse_tree) return ''.join(parts) -def inverseTag(name, parsed, template, delims): - def func(self): - data = self.get(name) +def _make_get_inverse(name, parsed, template, delims): + def get_inverse(context): + data = context.get(name) if data: return '' - return render_parse_tree(parsed, self, delims) - return func + return render_parse_tree(parsed, context) + + return get_inverse class EndOfSection(Exception): @@ -177,7 +183,7 @@ class RenderEngine(object): raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) parse_tree = self.parse_string_to_tree(template_string=template) - return render_parse_tree(parse_tree, context, template) + return render_parse_tree(parse_tree, context) def parse_string_to_tree(self, template_string, delims=None): @@ -255,33 +261,36 @@ class RenderEngine(object): return val - def escape_tag_function(self, name): - get_literal = self.literal_tag_function(name) - def func(context): + def _make_get_literal(self, name): + def get_literal(context): s = self._get_string_value(context, name) - s = self.escape(s) + s = self.literal(s) return s - return func - def literal_tag_function(self, name): - def func(context): + return get_literal + + def _make_get_escaped(self, name): + get_literal = self._make_get_literal(name) + + def get_escaped(context): s = self._get_string_value(context, name) - s = self.literal(s) + s = self.escape(s) return s - return func + return get_escaped - def partial_tag_function(self, name, indentation=''): - def func(context): + def _make_get_partial(self, name, indentation=''): + def get_partial(context): nonblank = re.compile(r'^(.)', re.M) template = self.load_partial(name) # Indent before rendering. template = re.sub(nonblank, indentation + r'\1', template) return self._render_template(template, context) - return func - def section_tag_function(self, name, parse_tree_, template_, delims): - def func(context): + return get_partial + + def _make_get_section(self, name, parse_tree_, template_, delims): + def get_section(context): template = template_ parse_tree = parse_tree_ data = context.get(name) @@ -298,11 +307,12 @@ class RenderEngine(object): parts = [] for element in data: context.push(element) - parts.append(render_parse_tree(parse_tree, context, delims)) + parts.append(render_parse_tree(parse_tree, context)) context.pop() return ''.join(parts) - return func + + return get_section def parse_to_tree(self, template, index=0): """ @@ -366,7 +376,7 @@ class RenderEngine(object): return end_index if captures['tag'] == '>': - func = self.partial_tag_function(name, captures['whitespace']) + func = self._make_get_partial(name, captures['whitespace']) elif captures['tag'] in ['#', '^']: try: @@ -376,16 +386,16 @@ class RenderEngine(object): tmpl = e.template end_index = e.position - tag = self.section_tag_function if captures['tag'] == '#' else inverseTag + tag = self._make_get_section if captures['tag'] == '#' else _make_get_inverse func = tag(name, bufr, tmpl, (self.otag, self.ctag)) elif captures['tag'] in ['{', '&']: - func = self.literal_tag_function(name) + func = self._make_get_literal(name) elif captures['tag'] == '': - func = self.escape_tag_function(name) + func = self._make_get_escaped(name) elif captures['tag'] == '/': -- cgit v1.2.1 From 7a1d6e5af0b6290156a5f49ef6b68bfdc01b22b7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 00:45:03 -0800 Subject: Removed two unused arguments of _make_get_inverse(). --- pystache/renderengine.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 91c9ec0..8d40080 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -77,7 +77,7 @@ def render_parse_tree(parse_tree, context): return ''.join(parts) -def _make_get_inverse(name, parsed, template, delims): +def _make_get_inverse(name, parsed): def get_inverse(context): data = context.get(name) if data: @@ -386,8 +386,10 @@ class RenderEngine(object): tmpl = e.template end_index = e.position - tag = self._make_get_section if captures['tag'] == '#' else _make_get_inverse - func = tag(name, bufr, tmpl, (self.otag, self.ctag)) + if captures['tag'] == '#': + func = self._make_get_section(name, bufr, tmpl, (self.otag, self.ctag)) + else: + func = _make_get_inverse(name, bufr) elif captures['tag'] in ['{', '&']: -- cgit v1.2.1 From b222ce48cd7a6ac4e063391b93fa2a5d3a200750 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 00:46:04 -0800 Subject: Removed unused code paths from call(). --- pystache/renderengine.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 8d40080..6fa2cce 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -51,15 +51,8 @@ def call(val, view): if args_count is 0: val = val() - elif args_count is 1 and args[0] in ['self', 'context']: - val = val(view) - elif args_count is 1: - val = val(template) else: - val = val(view, template) - - if callable(val): - val = val(template) + val = val(view) if val is None: val = '' -- cgit v1.2.1 From c1a96ac3423a1871cf373b3757b6a3144bb9f423 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 00:58:27 -0800 Subject: Simplified call() even further. --- pystache/renderengine.py | 61 ++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 6fa2cce..bef5f8f 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -17,8 +17,10 @@ DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] -def call(val, view): +def call(val, context): """ + Returns: a string of type unicode. + Arguments: val: the argument val can be any of the following: @@ -28,53 +30,48 @@ def call(val, view): * RenderEngine._make_get_literal(): Args: context + Returns: unicode * RenderEngine._make_get_escaped(): Args: context + Returns: unicode * RenderEngine._make_get_partial() Args: context + Returns: unicode * RenderEngine._make_get_section() Args: context + Returns: unicode * _make_get_inverse() Args: context + Returns: unicode """ if callable(val): - (args, _, _, _) = inspect.getargspec(val) - - args_count = len(args) - - if not isinstance(val, types.FunctionType): - # Then val is an instance method. Subtract one from the - # argument count because Python will automatically prepend - # self to the argument list when calling. - args_count -=1 - - if args_count is 0: - val = val() - else: - val = val(view) - - if val is None: - val = '' + val = val(context) - return unicode(val) + return val def render_parse_tree(parse_tree, context): """ - Convert a parse-tree into a string. + Returns: a string of type unicode. """ get_string = lambda val: call(val, context) parts = map(get_string, parse_tree) - return ''.join(parts) + s = ''.join(parts) + + return unicode(s) def _make_get_inverse(name, parsed): def get_inverse(context): + """ + Returns a string with type unicode. + + """ data = context.get(name) if data: - return '' + return u'' return render_parse_tree(parsed, context) return get_inverse @@ -166,6 +163,8 @@ class RenderEngine(object): def _render_template(self, template, context): """ + Returns: a string of type unicode. + Arguments: template: template string @@ -256,6 +255,10 @@ class RenderEngine(object): def _make_get_literal(self, name): def get_literal(context): + """ + Returns: a string of type unicode. + + """ s = self._get_string_value(context, name) s = self.literal(s) return s @@ -266,6 +269,10 @@ class RenderEngine(object): get_literal = self._make_get_literal(name) def get_escaped(context): + """ + Returns: a string of type unicode. + + """ s = self._get_string_value(context, name) s = self.escape(s) return s @@ -274,6 +281,10 @@ class RenderEngine(object): def _make_get_partial(self, name, indentation=''): def get_partial(context): + """ + Returns: a string of type unicode. + + """ nonblank = re.compile(r'^(.)', re.M) template = self.load_partial(name) # Indent before rendering. @@ -284,6 +295,10 @@ class RenderEngine(object): def _make_get_section(self, name, parse_tree_, template_, delims): def get_section(context): + """ + Returns: a string of type unicode. + + """ template = template_ parse_tree = parse_tree_ data = context.get(name) @@ -303,7 +318,7 @@ class RenderEngine(object): parts.append(render_parse_tree(parse_tree, context)) context.pop() - return ''.join(parts) + return unicode(''.join(parts)) return get_section -- cgit v1.2.1 From 9c2048a10a8eebb9bdfa32bfdf5be8eaf177ee68 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 01:02:39 -0800 Subject: Removed call() function. --- pystache/renderengine.py | 59 +++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index bef5f8f..39a37a1 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -17,47 +17,34 @@ DEFAULT_TAG_CLOSING = '}}' END_OF_LINE_CHARACTERS = ['\r', '\n'] -def call(val, context): - """ - Returns: a string of type unicode. - - Arguments: - - val: the argument val can be any of the following: - - * a unicode string - * the return value of a call to any of the following: - - * RenderEngine._make_get_literal(): - Args: context - Returns: unicode - * RenderEngine._make_get_escaped(): - Args: context - Returns: unicode - * RenderEngine._make_get_partial() - Args: context - Returns: unicode - * RenderEngine._make_get_section() - Args: context - Returns: unicode - * _make_get_inverse() - Args: context - Returns: unicode - - """ - if callable(val): - val = val(context) - - return val - - def render_parse_tree(parse_tree, context): """ Returns: a string of type unicode. + The elements of parse_tree can be any of the following: + + * a unicode string + * the return value of a call to any of the following: + + * RenderEngine._make_get_literal(): + Args: context + Returns: unicode + * RenderEngine._make_get_escaped(): + Args: context + Returns: unicode + * RenderEngine._make_get_partial() + Args: context + Returns: unicode + * RenderEngine._make_get_section() + Args: context + Returns: unicode + * _make_get_inverse() + Args: context + Returns: unicode + """ - get_string = lambda val: call(val, context) - parts = map(get_string, parse_tree) + get_unicode = lambda val: val(context) if callable(val) else val + parts = map(get_unicode, parse_tree) s = ''.join(parts) return unicode(s) -- cgit v1.2.1 From 6209032b63b49cce4e28646eb7864042d2ce0dc3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 12:19:45 -0800 Subject: Added Context.__repr__(). --- pystache/context.py | 13 +++++++++++++ tests/test_context.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pystache/context.py b/pystache/context.py index a3f9ff0..32c0a6a 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -112,6 +112,19 @@ class Context(object): """ self._stack = list(items) + def __repr__(self): + """ + Return a string representation of the instance. + + For example-- + + >>> context = Context({'alpha': 'abc'}, {'numeric': 123}) + >>> repr(context) + "Context({'alpha': 'abc'}, {'numeric': 123})" + + """ + return "%s%s" % (self.__class__.__name__, tuple(self._stack)) + @staticmethod def create(*context, **kwargs): """ diff --git a/tests/test_context.py b/tests/test_context.py index bf7517c..c737248 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -189,6 +189,26 @@ class ContextTests(TestCase): """ context = Context({}, {}, {}) + def test__repr(self): + context = Context() + self.assertEquals(repr(context), 'Context()') + + context = Context({'foo': 'bar'}) + self.assertEquals(repr(context), "Context({'foo': 'bar'},)") + + context = Context({'foo': 'bar'}, {'abc': 123}) + self.assertEquals(repr(context), "Context({'foo': 'bar'}, {'abc': 123})") + + def test__str(self): + context = Context() + self.assertEquals(str(context), 'Context()') + + context = Context({'foo': 'bar'}) + self.assertEquals(str(context), "Context({'foo': 'bar'},)") + + context = Context({'foo': 'bar'}, {'abc': 123}) + self.assertEquals(str(context), "Context({'foo': 'bar'}, {'abc': 123})") + ## Test the static create() method. def test_create__dictionary(self): -- cgit v1.2.1 From 62f8640d7ea92f72319abc277caeaad2b5f404b7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 13:01:29 -0800 Subject: Removed four imports from renderengine.py: cgi, collections, inspect, types. --- pystache/renderengine.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 39a37a1..c7fcb63 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -5,11 +5,7 @@ Defines a class responsible for rendering logic. """ -import cgi -import collections -import inspect import re -import types DEFAULT_TAG_OPENING = '{{' -- cgit v1.2.1 From 09363d96247269ba7475a5cafb5e0f06686cce87 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Jan 2012 13:37:43 -0800 Subject: Made non_blank regular expression an attribute. --- pystache/renderengine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index c7fcb63..2510e2a 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -91,6 +91,8 @@ class RenderEngine(object): otag = DEFAULT_TAG_OPENING ctag = DEFAULT_TAG_CLOSING + nonblank_re = re.compile(r'^(.)', re.M) + def __init__(self, load_partial=None, literal=None, escape=None): """ Arguments: @@ -268,10 +270,9 @@ class RenderEngine(object): Returns: a string of type unicode. """ - nonblank = re.compile(r'^(.)', re.M) template = self.load_partial(name) # Indent before rendering. - template = re.sub(nonblank, indentation + r'\1', template) + template = re.sub(self.nonblank_re, indentation + r'\1', template) return self._render_template(template, context) return get_partial -- cgit v1.2.1 From 9bf4ed053d1dc182000717488f61e8e5123e8405 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 3 Jan 2012 12:27:19 -0800 Subject: Added and improved test cases for testing literals. --- tests/test_renderengine.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 841b4d9..60996c7 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -164,20 +164,41 @@ class RenderTests(unittest.TestCase): Test an implicit iterator in a literal tag. """ - template = """{{#test}}{{.}}{{/test}}""" - context = {'test': ['a', 'b']} + template = """{{#test}}{{{.}}}{{/test}}""" + context = {'test': ['<', '>']} - self._assert_render('ab', template, context) + self._assert_render('<>', template, context) def test__implicit_iterator__escaped(self): """ Test an implicit iterator in a normal tag. """ - template = """{{#test}}{{{.}}}{{/test}}""" - context = {'test': ['a', 'b']} + template = """{{#test}}{{.}}{{/test}}""" + context = {'test': ['<', '>']} + + self._assert_render('<>', template, context) + + def test__literal__in_section(self): + """ + Check that literals work in sections. + + """ + template = '{{#test}}1 {{{less_than}}} 2{{/test}}' + context = {'test': {'less_than': '<'}} + + self._assert_render('1 < 2', template, context) + + def test__literal__in_partial(self): + """ + Check that literals work in partials. + + """ + template = '{{>partial}}' + partials = {'partial': '1 {{{less_than}}} 2'} + context = {'less_than': '<'} - self._assert_render('ab', template, context) + self._assert_render('1 < 2', template, context, partials=partials) def test_render_with_partial(self): partials = {'partial': "{{person}}"} -- cgit v1.2.1 From 883c9efcb727cdc7aa56026c9f9886abe1064f52 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 4 Jan 2012 21:25:44 -0800 Subject: Tweaked the RenderTests.test_section__list_referencing_outer_context() test. --- tests/test_renderengine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 60996c7..c11ad31 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -229,7 +229,7 @@ class RenderTests(unittest.TestCase): self._assert_render('unescaped: < escaped: <', template, context, engine=engine, partials=partials) - def test_render__list_referencing_outer_context(self): + def test_section__list_referencing_outer_context(self): """ Check that list items can access the parent context. @@ -239,13 +239,13 @@ class RenderTests(unittest.TestCase): """ context = { - "list": [{"name": "Al"}, {"name": "Bo"}], "greeting": "Hi", + "list": [{"name": "Al"}, {"name": "Bob"}], } - template = "{{#list}}{{name}}: {{greeting}}; {{/list}}" + template = "{{#list}}{{greeting}}, {{name}}; {{/list}}" - self._assert_render("Al: Hi; Bo: Hi; ", template, context) + self._assert_render("Hi, Al; Hi, Bob; ", template, context) def test_render__tag_in_value(self): """ -- cgit v1.2.1 From 804c87128adc5b6575258b4519cf1e64331d57c6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 7 Jan 2012 17:31:22 -0800 Subject: Added two rendering test cases re: sections. The two test cases test, respectively, (1) context precedence and (2) that section output not be rendered. The two test cases were originally proposed for inclusion in the Mustache spec test cases in mustache/spec issues #31 and #32: * https://github.com/mustache/spec/pull/31 * https://github.com/mustache/spec/pull/32 --- tests/test_renderengine.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index c11ad31..2a06a48 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -317,6 +317,24 @@ class RenderTests(unittest.TestCase): self._assert_render(expected, '{{=$ $=}} {{foo}} ') self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. + def test_section__output_not_interpolated(self): + """ + Check that rendered section output is not interpolated. + + """ + template = '{{#section}}{{template}}{{/section}}: {{planet}}' + context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} + self._assert_render(u'{{planet}}: Earth', template, context) + + def test_section__context_precedence(self): + """ + Check that items higher in the context stack take precedence. + + """ + template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}' + context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}} + self._assert_render(u'chicken : beans and rice', template, context) + def test_sections__nested_truthy(self): """ Check that "nested truthy" sections get rendered. -- cgit v1.2.1 From abea8bad12913eb4b8ab8373d876cdcec6f8d2b2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 7 Jan 2012 18:18:48 -0800 Subject: Reorganized (renamed and reordered) some rendering test cases. --- tests/test_renderengine.py | 158 +++++++++++++++++++++++---------------------- 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 2a06a48..4da2381 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -159,7 +159,25 @@ class RenderTests(unittest.TestCase): self._assert_render('FOO 100 100', template, context, engine=engine) - def test__implicit_iterator__literal(self): + def test_tag__output_not_interpolated(self): + """ + Context values should not be treated as templates (issue #44). + + """ + template = '{{test}}' + context = {'test': '{{hello}}'} + self._assert_render('{{hello}}', template, context) + + def test_tag__output_not_interpolated__section(self): + """ + Context values should not be treated as templates (issue #44). + + """ + template = '{{test}}' + context = {'test': '{{#hello}}'} + self._assert_render('{{#hello}}', template, context) + + def test_implicit_iterator__literal(self): """ Test an implicit iterator in a literal tag. @@ -169,7 +187,7 @@ class RenderTests(unittest.TestCase): self._assert_render('<>', template, context) - def test__implicit_iterator__escaped(self): + def test_implicit_iterator__escaped(self): """ Test an implicit iterator in a normal tag. @@ -179,7 +197,7 @@ class RenderTests(unittest.TestCase): self._assert_render('<>', template, context) - def test__literal__in_section(self): + def test_literal__in_section(self): """ Check that literals work in sections. @@ -189,7 +207,7 @@ class RenderTests(unittest.TestCase): self._assert_render('1 < 2', template, context) - def test__literal__in_partial(self): + def test_literal__in_partial(self): """ Check that literals work in partials. @@ -200,11 +218,26 @@ class RenderTests(unittest.TestCase): self._assert_render('1 < 2', template, context, partials=partials) - def test_render_with_partial(self): + def test_partial(self): partials = {'partial': "{{person}}"} self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) - def test_render__section_context_values(self): + def test_partial__context_values(self): + """ + Test that escape and literal work on context values in partials. + + """ + engine = self._engine() + + template = '{{>partial}}' + partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} + context = {'foo': '<'} + + self._assert_render('unescaped: < escaped: <', template, context, engine=engine, partials=partials) + + ## Test cases related specifically to sections. + + def test_section__context_values(self): """ Test that escape and literal work on context values in sections. @@ -216,18 +249,14 @@ class RenderTests(unittest.TestCase): self._assert_render('unescaped: < escaped: <', template, context, engine=engine) - def test_render__partial_context_values(self): + def test_section__context_precedence(self): """ - Test that escape and literal work on context values in partials. + Check that items higher in the context stack take precedence. """ - engine = self._engine() - - template = '{{>partial}}' - partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} - context = {'foo': '<'} - - self._assert_render('unescaped: < escaped: <', template, context, engine=engine, partials=partials) + template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}' + context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}} + self._assert_render(u'chicken : beans and rice', template, context) def test_section__list_referencing_outer_context(self): """ @@ -243,34 +272,55 @@ class RenderTests(unittest.TestCase): "list": [{"name": "Al"}, {"name": "Bob"}], } - template = "{{#list}}{{greeting}}, {{name}}; {{/list}}" + template = "{{#list}}{{greeting}} {{name}}, {{/list}}" - self._assert_render("Hi, Al; Hi, Bob; ", template, context) + self._assert_render("Hi Al, Hi Bob, ", template, context) - def test_render__tag_in_value(self): + def test_section__output_not_interpolated(self): """ - Context values should not be treated as templates (issue #44). + Check that rendered section output is not interpolated. """ - template = '{{test}}' - context = {'test': '{{hello}}'} - self._assert_render('{{hello}}', template, context) + template = '{{#section}}{{template}}{{/section}}: {{planet}}' + context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} + self._assert_render(u'{{planet}}: Earth', template, context) - def test_render__section_in_value(self): + def test_section__nested_truthy(self): """ - Context values should not be treated as templates (issue #44). + Check that "nested truthy" sections get rendered. + + Test case for issue #24: https://github.com/defunkt/pystache/issues/24 + + This test is copied from the spec. We explicitly include it to + prevent regressions for those who don't pull down the spec tests. """ - template = '{{test}}' - context = {'test': '{{#hello}}'} - self._assert_render('{{#hello}}', template, context) + template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |' + context = {'bool': True} + self._assert_render(u'| A B C D E |', template, context) + + def test_section__nested_with_same_keys(self): + """ + Check a doubly-nested section with the same context key. + + Test case for issue #36: https://github.com/defunkt/pystache/issues/36 + + """ + # Start with an easier, working case. + template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}' + context = {'x': {'z': {'y': 1}}} + self._assert_render(u'1', template, context) + + template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}' + context = {'x': {'x': {'y': 1}}} + self._assert_render(u'1', template, context) - def test_render__section__lambda(self): + def test_section__lambda(self): template = '{{#test}}Mom{{/test}}' context = {'test': (lambda text: 'Hi %s' % text)} self._assert_render('Hi Mom', template, context) - def test_render__section__lambda__tag_in_output(self): + def test_section__lambda__tag_in_output(self): """ Check that callable output is treated as a template string (issue #46). @@ -287,7 +337,7 @@ class RenderTests(unittest.TestCase): context = {'person': 'Mom', 'test': (lambda text: text + " :)")} self._assert_render('Hi Mom :)', template, context) - def test_render__section__comment__multiline(self): + def test_comment__multiline(self): """ Check that multiline comments are permitted. @@ -316,51 +366,3 @@ class RenderTests(unittest.TestCase): expected = u' {{foo}} ' self._assert_render(expected, '{{=$ $=}} {{foo}} ') self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. - - def test_section__output_not_interpolated(self): - """ - Check that rendered section output is not interpolated. - - """ - template = '{{#section}}{{template}}{{/section}}: {{planet}}' - context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} - self._assert_render(u'{{planet}}: Earth', template, context) - - def test_section__context_precedence(self): - """ - Check that items higher in the context stack take precedence. - - """ - template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}' - context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}} - self._assert_render(u'chicken : beans and rice', template, context) - - def test_sections__nested_truthy(self): - """ - Check that "nested truthy" sections get rendered. - - Test case for issue #24: https://github.com/defunkt/pystache/issues/24 - - This test is copied from the spec. We explicitly include it to - prevent regressions for those who don't pull down the spec tests. - - """ - template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |' - context = {'bool': True} - self._assert_render(u'| A B C D E |', template, context) - - def test_sections__nested_with_same_keys(self): - """ - Check a doubly-nested section with the same context key. - - Test case for issue #36: https://github.com/defunkt/pystache/issues/36 - - """ - # Start with an easier, working case. - template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}' - context = {'x': {'z': {'y': 1}}} - self._assert_render(u'1', template, context) - - template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}' - context = {'x': {'x': {'y': 1}}} - self._assert_render(u'1', template, context) -- cgit v1.2.1 From 3e284008bfd08fd661b5d76c6928a23a9a772d9b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 9 Jan 2012 01:55:16 -0800 Subject: Tweaked test case that interpolation output should not be reinterpolated. --- tests/test_renderengine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 4da2381..2a37864 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -164,9 +164,9 @@ class RenderTests(unittest.TestCase): Context values should not be treated as templates (issue #44). """ - template = '{{test}}' - context = {'test': '{{hello}}'} - self._assert_render('{{hello}}', template, context) + template = '{{template}}: {{planet}}' + context = {'template': '{{planet}}', 'planet': 'Earth'} + self._assert_render(u'{{planet}}: Earth', template, context) def test_tag__output_not_interpolated__section(self): """ -- cgit v1.2.1 From 4b8a6952a16076c7625db20affcae8f6876df18d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 15 Jan 2012 15:21:41 -0800 Subject: Changed the test_context.TestCase class to a mixin class. --- tests/test_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index c737248..3483d5c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -12,9 +12,9 @@ from pystache.context import _get_item from pystache.context import Context -class TestCase(unittest.TestCase): +class AssertIsMixin: - """A TestCase class with support for assertIs().""" + """A mixin for adding assertIs() to a unittest.TestCase.""" # unittest.assertIs() is not available until Python 2.7: # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone @@ -48,7 +48,7 @@ class MappingObject(object): return self._dict[key] -class GetItemTestCase(TestCase): +class GetItemTestCase(unittest.TestCase, AssertIsMixin): """Test context._get_item().""" @@ -168,7 +168,7 @@ class GetItemTestCase(TestCase): self.assertRaises(AttributeError, _get_item, obj, "foo") -class ContextTests(TestCase): +class ContextTests(unittest.TestCase, AssertIsMixin): """ Test the Context class. -- cgit v1.2.1 From ee579e2166ce76e83843f75692e70e208f319ca9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 17 Jan 2012 08:17:12 -0800 Subject: Addresses issue #81: not to call methods on instances of built-in types. --- pystache/context.py | 202 ++++++++++++++++++++++++-------------------------- tests/test_context.py | 154 +++++++++++++++++++++++--------------- 2 files changed, 193 insertions(+), 163 deletions(-) diff --git a/pystache/context.py b/pystache/context.py index 32c0a6a..1621d61 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -5,11 +5,12 @@ Defines a Context class to represent mustache(5)'s notion of context. """ +class NotFound(object): pass # We use this private global variable as a return value to represent a key # not being found on lookup. This lets us distinguish between the case # of a key's value being None with the case of a key not being found -- # without having to rely on exceptions (e.g. KeyError) for flow control. -_NOT_FOUND = object() +_NOT_FOUND = NotFound() # TODO: share code with template.check_callable(). @@ -17,40 +18,35 @@ def _is_callable(obj): return hasattr(obj, '__call__') -def _get_item(obj, key): +def _get_value(item, key): """ - Return a key's value, or _NOT_FOUND if the key does not exist. + Retrieve a key's value from an item. - The obj argument should satisfy the same conditions as those - described for the arguments passed to Context.__init__(). These - conditions are described in Context.__init__()'s docstring. + Returns _NOT_FOUND if the key does not exist. - The rules for looking up the value of a key are the same as the rules - described in Context.get()'s docstring for querying a single item. - - The behavior of this function is undefined if obj is None. + The Context.get() docstring documents this function's intended behavior. """ - if hasattr(obj, '__getitem__'): - # We do a membership test to avoid using exceptions for flow control - # (e.g. catching KeyError). In addition, we call __contains__() - # explicitly as opposed to using the membership operator "in" to - # avoid triggering the following Python fallback behavior: + if isinstance(item, dict): + # Then we consider the argument a "hash" for the purposes of the spec. # - # "For objects that don’t define __contains__(), the membership test - # first tries iteration via __iter__(), then the old sequence - # iteration protocol via __getitem__()...." + # We do a membership test to avoid using exceptions for flow control + # (e.g. catching KeyError). + if key in item: + return item[key] + elif type(item).__module__ != '__builtin__': + # Then we consider the argument an "object" for the purposes of + # the spec. # - # (from http://docs.python.org/reference/datamodel.html#object.__contains__ ) - if obj.__contains__(key): - return obj[key] - - elif hasattr(obj, key): - attr = getattr(obj, key) - if _is_callable(attr): - return attr() - - return attr + # The elif test above lets us avoid treating instances of built-in + # types like integers and strings as objects (cf. issue #81). + # Instances of user-defined classes on the other hand, for example, + # are considered objects by the test above. + if hasattr(item, key): + attr = getattr(item, key) + if _is_callable(attr): + return attr() + return attr return _NOT_FOUND @@ -61,18 +57,18 @@ class Context(object): Provides dictionary-like access to a stack of zero or more items. Instances of this class are meant to act as the rendering context - when rendering mustache templates in accordance with mustache(5). - - Instances encapsulate a private stack of objects and dictionaries. - Querying the stack for the value of a key queries the items in the - stack in order from last-added objects to first (last in, first out). + when rendering Mustache templates in accordance with mustache(5) + and the Mustache spec. - *Caution*: + Instances encapsulate a private stack of hashes, objects, and built-in + type instances. Querying the stack for the value of a key queries + the items in the stack in order from last-added objects to first + (last in, first out). - This class currently does not support recursive nesting in that - items in the stack cannot themselves be Context instances. + Caution: this class does not currently support recursive nesting in + that items in the stack cannot themselves be Context instances. - See the docstrings of the methods of this class for more information. + See the docstrings of the methods of this class for more details. """ @@ -87,27 +83,8 @@ class Context(object): stack in order so that, in particular, items at the end of the argument list are queried first when querying the stack. - Each item should satisfy the following condition: - - * If the item implements __getitem__(), it should also implement - __contains__(). Failure to implement __contains__() will cause - an AttributeError to be raised when the item is queried during - calls to self.get(). - - Python dictionaries, in particular, satisfy this condition. - An item satisfying this condition we informally call a "mapping - object" because it shares some characteristics of the Mapping - abstract base class (ABC) in Python's collections package: - http://docs.python.org/library/collections.html#collections-abstract-base-classes - - It is not necessary for an item to implement __getitem__(). - In particular, an item can be an ordinary object with no - mapping-like characteristics. - - *Caution*: - - Items should not themselves be Context instances, as recursive - nesting does not behave as one might expect. + Caution: items should not themselves be Context instances, as + recursive nesting does not behave as one might expect. """ self._stack = list(items) @@ -128,11 +105,11 @@ class Context(object): @staticmethod def create(*context, **kwargs): """ - Build a Context instance from a sequence of "mapping-like" objects. + Build a Context instance from a sequence of context-like items. This factory-style method is more general than the Context class's - constructor in that Context instances can themselves appear in the - argument list. This is not true of the constructor. + constructor in that, unlike the constructor, the argument list + can itself contain Context instances. Here is an example illustrating various aspects of this method: @@ -185,56 +162,71 @@ class Context(object): """ Query the stack for the given key, and return the resulting value. - Querying for a key queries items in the stack in order from last- - added objects to first (last in, first out). The value returned - is the value of the key for the first item for which the item - contains the key. If the key is not found in any item in the - stack, then this method returns the default value. The default - value defaults to None. - - Querying an item in the stack is done in the following way: - - (1) If the item defines __getitem__() and the item contains the - key (i.e. __contains__() returns True), then the corresponding - value is returned. - (2) Otherwise, the method looks for an attribute with the same - name as the key. If such an attribute exists, the value of - this attribute is returned. If the attribute is callable, - however, the attribute is first called with no arguments. - (3) If there is no attribute with the same name as the key, then - the key is considered not found in the item. + This method queries items in the stack in order from last-added + objects to first (last in, first out). The value returned is + the value of the key in the first item that contains the key. + If the key is not found in any item in the stack, then the default + value is returned. The default value defaults to None. + + When speaking about returning values from a context, the Mustache + spec distinguishes between two types of context stack elements: + hashes and objects. + + In accordance with the spec, this method queries items in the + stack for a key in the following way. For the purposes of querying, + each item is classified into one of the following three mutually + exclusive categories: a hash, an object, or neither: + + (1) Hash: if the item's type is a subclass of dict, then the item + is considered a hash (in the terminology of the spec), and + the key's value is the dictionary value of the key. If the + dictionary doesn't contain the key, the key is not found. + + (2) Object: if the item isn't a hash and isn't an instance of a + built-in type, then the item is considered an object (again + using the language of the spec). In this case, the method + looks for an attribute with the same name as the key. If an + attribute with that name exists, the value of the attribute is + returned. If the attribute is callable, however (i.e. if the + attribute is a method), then the attribute is called with no + arguments and instead that value returned. If there is no + attribute with the same name as the key, then the key is + considered not found. + + (3) Neither: if the item is neither a hash nor an object, then + the key is considered not found. *Caution*: - Callables resulting from a call to __getitem__ (as in (1) - above) are handled differently from callables that are merely - attributes (as in (2) above). - - The former are returned as-is, while the latter are first called - and that value returned. Here is an example: - - >>> def greet(): - ... return "Hi Bob!" - >>> - >>> class Greeter(object): - ... greet = None - >>> - >>> obj = Greeter() - >>> obj.greet = greet - >>> dct = {'greet': greet} - >>> - >>> obj.greet is dct['greet'] - True - >>> Context(obj).get('greet') - 'Hi Bob!' - >>> Context(dct).get('greet') #doctest: +ELLIPSIS - - - TODO: explain the rationale for this difference in treatment. + Callables are handled differently depending on whether they are + dictionary values, as in (1) above, or attributes, as in (2). + The former are returned as-is, while the latter are first + called and that value returned. + + Here is an example to illustrate: + + >>> def greet(): + ... return "Hi Bob!" + >>> + >>> class Greeter(object): + ... greet = None + >>> + >>> dct = {'greet': greet} + >>> obj = Greeter() + >>> obj.greet = greet + >>> + >>> dct['greet'] is obj.greet + True + >>> Context(dct).get('greet') #doctest: +ELLIPSIS + + >>> Context(obj).get('greet') + 'Hi Bob!' + + TODO: explain the rationale for this difference in treatment. """ for obj in reversed(self._stack): - val = _get_item(obj, key) + val = _get_value(obj, key) if val is _NOT_FOUND: continue # Otherwise, the key was found. diff --git a/tests/test_context.py b/tests/test_context.py index 3483d5c..3486251 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -5,23 +5,14 @@ Unit tests of context.py. """ +from datetime import datetime import unittest from pystache.context import _NOT_FOUND -from pystache.context import _get_item +from pystache.context import _get_value from pystache.context import Context -class AssertIsMixin: - - """A mixin for adding assertIs() to a unittest.TestCase.""" - - # unittest.assertIs() is not available until Python 2.7: - # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone - def assertIs(self, first, second): - self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) - - class SimpleObject(object): """A sample class that does not define __getitem__().""" @@ -33,7 +24,7 @@ class SimpleObject(object): return "called..." -class MappingObject(object): +class DictLike(object): """A sample class that implements __getitem__() and __contains__().""" @@ -48,26 +39,36 @@ class MappingObject(object): return self._dict[key] -class GetItemTestCase(unittest.TestCase, AssertIsMixin): +class AssertIsMixin: + + """A mixin for adding assertIs() to a unittest.TestCase.""" + + # unittest.assertIs() is not available until Python 2.7: + # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone + def assertIs(self, first, second): + self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) + - """Test context._get_item().""" +class GetValueTests(unittest.TestCase, AssertIsMixin): - def assertNotFound(self, obj, key): + """Test context._get_value().""" + + def assertNotFound(self, item, key): """ - Assert that a call to _get_item() returns _NOT_FOUND. + Assert that a call to _get_value() returns _NOT_FOUND. """ - self.assertIs(_get_item(obj, key), _NOT_FOUND) + self.assertIs(_get_value(item, key), _NOT_FOUND) - ### Case: obj is a dictionary. + ### Case: the item is a dictionary. def test_dictionary__key_present(self): """ Test getting a key from a dictionary. """ - obj = {"foo": "bar"} - self.assertEquals(_get_item(obj, "foo"), "bar") + item = {"foo": "bar"} + self.assertEquals(_get_value(item, "foo"), "bar") def test_dictionary__callable_not_called(self): """ @@ -77,95 +78,132 @@ class GetItemTestCase(unittest.TestCase, AssertIsMixin): def foo_callable(self): return "bar" - obj = {"foo": foo_callable} - self.assertNotEquals(_get_item(obj, "foo"), "bar") - self.assertTrue(_get_item(obj, "foo") is foo_callable) + item = {"foo": foo_callable} + self.assertNotEquals(_get_value(item, "foo"), "bar") + self.assertTrue(_get_value(item, "foo") is foo_callable) def test_dictionary__key_missing(self): """ Test getting a missing key from a dictionary. """ - obj = {} - self.assertNotFound(obj, "missing") + item = {} + self.assertNotFound(item, "missing") def test_dictionary__attributes_not_checked(self): """ Test that dictionary attributes are not checked. """ - obj = {} + item = {} attr_name = "keys" - self.assertEquals(getattr(obj, attr_name)(), []) - self.assertNotFound(obj, attr_name) + self.assertEquals(getattr(item, attr_name)(), []) + self.assertNotFound(item, attr_name) + + def test_dictionary__dict_subclass(self): + """ + Test that subclasses of dict are treated as dictionaries. + + """ + class DictSubclass(dict): pass - ### Case: obj does not implement __getitem__(). + item = DictSubclass() + item["foo"] = "bar" + + self.assertEquals(_get_value(item, "foo"), "bar") + + ### Case: the item is an object. def test_object__attribute_present(self): """ Test getting an attribute from an object. """ - obj = SimpleObject() - self.assertEquals(_get_item(obj, "foo"), "bar") + item = SimpleObject() + self.assertEquals(_get_value(item, "foo"), "bar") def test_object__attribute_missing(self): """ Test getting a missing attribute from an object. """ - obj = SimpleObject() - self.assertNotFound(obj, "missing") + item = SimpleObject() + self.assertNotFound(item, "missing") def test_object__attribute_is_callable(self): """ Test getting a callable attribute from an object. """ - obj = SimpleObject() - self.assertEquals(_get_item(obj, "foo_callable"), "called...") + item = SimpleObject() + self.assertEquals(_get_value(item, "foo_callable"), "called...") + + def test_object__non_built_in_type(self): + """ + Test getting an attribute from an instance of a type that isn't built-in. - ### Case: obj implements __getitem__() (i.e. a "mapping object"). + """ + item = datetime(2012, 1, 2) + self.assertEquals(_get_value(item, "day"), 2) - def test_mapping__key_present(self): + def test_object__dict_like(self): """ - Test getting a key from a mapping object. + Test getting a key from a dict-like object (an object that implements '__getitem__'). """ - obj = MappingObject() - self.assertEquals(_get_item(obj, "foo"), "bar") + item = DictLike() + self.assertEquals(item["foo"], "bar") + self.assertNotFound(item, "foo") + + ### Case: the item is an instance of a built-in type. - def test_mapping__key_missing(self): + def test_built_in_type__integer(self): """ - Test getting a missing key from a mapping object. + Test getting from an integer. """ - obj = MappingObject() - self.assertNotFound(obj, "missing") + class MyInt(int): pass + + item1 = MyInt(10) + item2 = 10 + + self.assertEquals(item1.real, 10) + self.assertEquals(item2.real, 10) + + self.assertEquals(_get_value(item1, 'real'), 10) + self.assertNotFound(item2, 'real') - def test_mapping__get_attribute(self): + def test_built_in_type__string(self): """ - Test getting an attribute from a mapping object. + Test getting from a string. """ - obj = MappingObject() - key = "fuzz" - self.assertEquals(getattr(obj, key), "buzz") - # As desired, __getitem__()'s presence causes obj.fuzz not to be checked. - self.assertNotFound(obj, key) + class MyStr(str): pass - def test_mapping_object__not_implementing_contains(self): + item1 = MyStr('abc') + item2 = 'abc' + + self.assertEquals(item1.upper(), 'ABC') + self.assertEquals(item2.upper(), 'ABC') + + self.assertEquals(_get_value(item1, 'upper'), 'ABC') + self.assertNotFound(item2, 'upper') + + def test_built_in_type__list(self): """ - Test querying a mapping object that doesn't define __contains__(). + Test getting from a list. """ - class Sample(object): + class MyList(list): pass + + item1 = MyList([1, 2, 3]) + item2 = [1, 2, 3] - def __getitem__(self, key): - return "bar" + self.assertEquals(item1.pop(), 3) + self.assertEquals(item2.pop(), 3) - obj = Sample() - self.assertRaises(AttributeError, _get_item, obj, "foo") + self.assertEquals(_get_value(item1, 'pop'), 2) + self.assertNotFound(item2, 'pop') class ContextTests(unittest.TestCase, AssertIsMixin): -- cgit v1.2.1 From 54709f8d48d32b2aa8a362f8609476e02f7df8f6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 17 Jan 2012 09:15:59 -0800 Subject: Added some RenderEngine test cases for issue #81. --- tests/test_renderengine.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 2a37864..8df23f8 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -177,6 +177,39 @@ class RenderTests(unittest.TestCase): context = {'test': '{{#hello}}'} self._assert_render('{{#hello}}', template, context) + def test_interpolation__built_in_type__string(self): + """ + Check tag interpolation with a string on the top of the context stack. + + """ + item = 'abc' + # item.upper() == 'ABC' + template = '{{#section}}{{upper}}{{/section}}' + context = {'section': item, 'upper': 'XYZ'} + self._assert_render(u'XYZ', template, context) + + def test_interpolation__built_in_type__integer(self): + """ + Check tag interpolation with an integer on the top of the context stack. + + """ + item = 10 + # item.real == 10 + template = '{{#section}}{{real}}{{/section}}' + context = {'section': item, 'real': 1000} + self._assert_render(u'1000', template, context) + + def test_interpolation__built_in_type__list(self): + """ + Check tag interpolation with a list on the top of the context stack. + + """ + item = [[1, 2, 3]] + # item[0].pop() == 3 + template = '{{#section}}{{pop}}{{/section}}' + context = {'section': item, 'pop': 7} + self._assert_render(u'7', template, context) + def test_implicit_iterator__literal(self): """ Test an implicit iterator in a literal tag. -- cgit v1.2.1 From 19a3531ae1e3c83158d87f2bce121ff7ca9ba5ce Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 13:30:42 -0800 Subject: Added to RenderEngine.parse_to_tree() docstring. --- pystache/renderengine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 2510e2a..4bf4a27 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -308,7 +308,10 @@ class RenderEngine(object): def parse_to_tree(self, template, index=0): """ - Parse a template into a syntax tree. + Parse a template string into a syntax tree using current attributes. + + This method uses the current RenderEngine instance's attributes, + including the current tag delimiter, etc. """ parse_tree = [] -- cgit v1.2.1 From aea32c6ea1e3f44ca00a5578b2903c8b84ec3287 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 17:47:05 -0800 Subject: Refactored some of the code involving compiling the main regular expression. --- pystache/renderengine.py | 92 +++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 4bf4a27..7956b20 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -8,11 +8,36 @@ Defines a class responsible for rendering logic. import re -DEFAULT_TAG_OPENING = '{{' -DEFAULT_TAG_CLOSING = '}}' +DEFAULT_DELIMITERS = ('{{', '}}') END_OF_LINE_CHARACTERS = ['\r', '\n'] +def _compile_template_re(delimiters): + + # The possible tag type characters following the opening tag, + # excluding "=" and "{". + tag_types = "!>&/#^" + + # TODO: are we following this in the spec? + # + # The tag's content MUST be a non-whitespace character sequence + # NOT containing the current closing delimiter. + # + tag = r""" + (?P[\s\S]*?) + (?P[\ \t]*) + %(otag)s \s* + (?: + (?P=) \s* (?P.+?) \s* = | + (?P{) \s* (?P.+?) \s* } | + (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) + ) + \s* %(ctag)s + """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} + + return re.compile(tag, re.M | re.X) + + def render_parse_tree(parse_tree, context): """ Returns: a string of type unicode. @@ -86,14 +111,12 @@ class RenderEngine(object): """ - tag_re = None - - otag = DEFAULT_TAG_OPENING - ctag = DEFAULT_TAG_CLOSING + _delimiters = None + _template_re = None nonblank_re = re.compile(r'^(.)', re.M) - def __init__(self, load_partial=None, literal=None, escape=None): + def __init__(self, load_partial=None, literal=None, escape=None, delimiters=None): """ Arguments: @@ -123,10 +146,23 @@ class RenderEngine(object): from plain unicode strings. """ + if delimiters is None: + delimiters = DEFAULT_DELIMITERS + self.escape = escape self.literal = literal self.load_partial = load_partial + # TODO: consider an approach that doesn't require compiling a regular + # expression in the constructor. For example, be lazier. That way + # rendering will still work as expected even if the delimiters are + # set after the constructor + self._set_delimiters(delimiters) + + def _set_delimiters(self, delimiters): + self._delimiters = delimiters + self._template_re = _compile_template_re(self._delimiters) + def render(self, template, context): """ Return a template rendered as a string with type unicode. @@ -165,42 +201,10 @@ class RenderEngine(object): def parse_string_to_tree(self, template_string, delims=None): engine = RenderEngine(load_partial=self.load_partial, - literal=self.literal, - escape=self.escape) - - if delims is not None: - engine.otag = delims[0] - engine.ctag = delims[1] - - engine._compile_regexps() + literal=self.literal, escape=self.escape, delimiters=delims) return engine.parse_to_tree(template=template_string) - def _compile_regexps(self): - - # The possible tag type characters following the opening tag, - # excluding "=" and "{". - tag_types = "!>&/#^" - - # TODO: are we following this in the spec? - # - # The tag's content MUST be a non-whitespace character sequence - # NOT containing the current closing delimiter. - # - tag = r""" - (?P[\s\S]*?) - (?P[\ \t]*) - %(otag)s \s* - (?: - (?P=) \s* (?P.+?) \s* = | - (?P{) \s* (?P.+?) \s* } | - (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) - ) - \s* %(ctag)s - """ % {'tag_types': tag_types, 'otag': re.escape(self.otag), 'ctag': re.escape(self.ctag)} - - self.tag_re = re.compile(tag, re.M | re.X) - def _get_string_value(self, context, tag_name): """ Get a value from the given context as a basestring instance. @@ -318,7 +322,7 @@ class RenderEngine(object): start_index = index while True: - match = self.tag_re.search(template, index) + match = self._template_re.search(template, index) if match is None: break @@ -366,8 +370,8 @@ class RenderEngine(object): return end_index if captures['tag'] == '=': - self.otag, self.ctag = name.split() - self._compile_regexps() + delimiters = name.split() + self._set_delimiters(delimiters) return end_index if captures['tag'] == '>': @@ -382,7 +386,7 @@ class RenderEngine(object): end_index = e.position if captures['tag'] == '#': - func = self._make_get_section(name, bufr, tmpl, (self.otag, self.ctag)) + func = self._make_get_section(name, bufr, tmpl, self._delimiters) else: func = _make_get_inverse(name, bufr) -- cgit v1.2.1 From 285f8485fc8e1a1eb96b619d76dd2bbe1cad0626 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 18:06:59 -0800 Subject: Addressed TODO to move regex compilation out of the constructor. --- pystache/renderengine.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 7956b20..763c81c 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -149,19 +149,17 @@ class RenderEngine(object): if delimiters is None: delimiters = DEFAULT_DELIMITERS + self._delimiters = delimiters self.escape = escape self.literal = literal self.load_partial = load_partial - # TODO: consider an approach that doesn't require compiling a regular - # expression in the constructor. For example, be lazier. That way - # rendering will still work as expected even if the delimiters are - # set after the constructor - self._set_delimiters(delimiters) + def _compile_template_re(self): + self._template_re = _compile_template_re(self._delimiters) - def _set_delimiters(self, delimiters): + def _change_delimiters(self, delimiters): self._delimiters = delimiters - self._template_re = _compile_template_re(self._delimiters) + self._compile_template_re() def render(self, template, context): """ @@ -202,6 +200,7 @@ class RenderEngine(object): engine = RenderEngine(load_partial=self.load_partial, literal=self.literal, escape=self.escape, delimiters=delims) + engine._compile_template_re() return engine.parse_to_tree(template=template_string) @@ -371,7 +370,7 @@ class RenderEngine(object): if captures['tag'] == '=': delimiters = name.split() - self._set_delimiters(delimiters) + self._change_delimiters(delimiters) return end_index if captures['tag'] == '>': -- cgit v1.2.1 From b5ed8a695babc1208f1b2e200819991a241c52d8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 20:17:05 -0800 Subject: Renamed the "captures" local variable to "matches". --- pystache/renderengine.py | 50 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 763c81c..f4d916e 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -326,56 +326,56 @@ class RenderEngine(object): if match is None: break - captures = match.groupdict() + matches = match.groupdict() match_index = match.end('content') end_index = match.end() - index = self._handle_match(template, parse_tree, captures, start_index, match_index, end_index) + index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) return parse_tree - def _handle_match(self, template, parse_tree, captures, start_index, match_index, end_index): + def _handle_match(self, template, parse_tree, matches, start_index, match_index, end_index): - # Normalize the captures dictionary. - if captures['change'] is not None: - captures.update(tag='=', name=captures['delims']) - elif captures['raw'] is not None: - captures.update(tag='{', name=captures['raw_name']) + # Normalize the matches dictionary. + if matches['change'] is not None: + matches.update(tag='=', name=matches['delims']) + elif matches['raw'] is not None: + matches.update(tag='{', name=matches['raw_name']) - parse_tree.append(captures['content']) + parse_tree.append(matches['content']) # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS - is_tag_interpolating = captures['tag'] in ['', '&', '{'] + is_tag_interpolating = matches['tag'] in ['', '&', '{'] if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: if end_index < len(template): end_index += template[end_index] == '\r' and 1 or 0 if end_index < len(template): end_index += template[end_index] == '\n' and 1 or 0 - elif captures['whitespace']: - parse_tree.append(captures['whitespace']) - match_index += len(captures['whitespace']) - captures['whitespace'] = '' + elif matches['whitespace']: + parse_tree.append(matches['whitespace']) + match_index += len(matches['whitespace']) + matches['whitespace'] = '' - name = captures['name'] + name = matches['name'] - if captures['tag'] == '!': + if matches['tag'] == '!': return end_index - if captures['tag'] == '=': + if matches['tag'] == '=': delimiters = name.split() self._change_delimiters(delimiters) return end_index - if captures['tag'] == '>': - func = self._make_get_partial(name, captures['whitespace']) - elif captures['tag'] in ['#', '^']: + if matches['tag'] == '>': + func = self._make_get_partial(name, matches['whitespace']) + elif matches['tag'] in ['#', '^']: try: self.parse_to_tree(template=template, index=end_index) @@ -384,26 +384,26 @@ class RenderEngine(object): tmpl = e.template end_index = e.position - if captures['tag'] == '#': + if matches['tag'] == '#': func = self._make_get_section(name, bufr, tmpl, self._delimiters) else: func = _make_get_inverse(name, bufr) - elif captures['tag'] in ['{', '&']: + elif matches['tag'] in ['{', '&']: func = self._make_get_literal(name) - elif captures['tag'] == '': + elif matches['tag'] == '': func = self._make_get_escaped(name) - elif captures['tag'] == '/': + elif matches['tag'] == '/': # TODO: don't use exceptions for flow control. raise EndOfSection(parse_tree, template[start_index:match_index], end_index) else: - raise Exception("'%s' is an unrecognized type!" % captures['tag']) + raise Exception("'%s' is an unrecognized type!" % matches['tag']) parse_tree.append(func) -- cgit v1.2.1 From f20ecb88e5f46bf055950efa9355179eb467f704 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 20:35:39 -0800 Subject: Removed unnecessary re.M from re.compile(). --- pystache/renderengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index f4d916e..4f2420c 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -35,7 +35,7 @@ def _compile_template_re(delimiters): \s* %(ctag)s """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} - return re.compile(tag, re.M | re.X) + return re.compile(tag, re.VERBOSE) def render_parse_tree(parse_tree, context): -- cgit v1.2.1 From 7cfa4b13cc1809ac45527bf5cfd9680c109d78e6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 21:10:50 -0800 Subject: Moved matches['content'] logic outside of _handle_match(). --- pystache/renderengine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 4f2420c..5869edc 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -327,9 +327,12 @@ class RenderEngine(object): break matches = match.groupdict() + match_index = match.end('content') end_index = match.end() + parse_tree.append(matches['content']) + index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) # Save the rest of the template. @@ -345,8 +348,6 @@ class RenderEngine(object): elif matches['raw'] is not None: matches.update(tag='{', name=matches['raw_name']) - parse_tree.append(matches['content']) - # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS -- cgit v1.2.1 From b808970c5521f2fbfface011c8af5395e5249564 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 21:39:56 -0800 Subject: Simplified the template regex: removed the "content" group. --- pystache/renderengine.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 5869edc..f13fe7f 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -24,7 +24,6 @@ def _compile_template_re(delimiters): # NOT containing the current closing delimiter. # tag = r""" - (?P[\s\S]*?) (?P[\ \t]*) %(otag)s \s* (?: @@ -326,12 +325,14 @@ class RenderEngine(object): if match is None: break - matches = match.groupdict() - - match_index = match.end('content') + match_index = match.start() end_index = match.end() - parse_tree.append(matches['content']) + before_tag = template[index : match_index] + + parse_tree.append(before_tag) + + matches = match.groupdict() index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) -- cgit v1.2.1 From 73306a888cafd563cea04f746fa30acaf8f4a210 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Jan 2012 22:31:32 -0800 Subject: Added "tag_type = matches['tag']" to _handle_match(). --- pystache/renderengine.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index f13fe7f..fecd937 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -349,11 +349,13 @@ class RenderEngine(object): elif matches['raw'] is not None: matches.update(tag='{', name=matches['raw_name']) + tag_type = matches['tag'] + # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS - is_tag_interpolating = matches['tag'] in ['', '&', '{'] + is_tag_interpolating = tag_type in ['', '&', '{'] if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: if end_index < len(template): @@ -367,17 +369,17 @@ class RenderEngine(object): name = matches['name'] - if matches['tag'] == '!': + if tag_type == '!': return end_index - if matches['tag'] == '=': + if tag_type == '=': delimiters = name.split() self._change_delimiters(delimiters) return end_index - if matches['tag'] == '>': + if tag_type == '>': func = self._make_get_partial(name, matches['whitespace']) - elif matches['tag'] in ['#', '^']: + elif tag_type in ['#', '^']: try: self.parse_to_tree(template=template, index=end_index) @@ -386,26 +388,26 @@ class RenderEngine(object): tmpl = e.template end_index = e.position - if matches['tag'] == '#': + if tag_type == '#': func = self._make_get_section(name, bufr, tmpl, self._delimiters) else: func = _make_get_inverse(name, bufr) - elif matches['tag'] in ['{', '&']: + elif tag_type in ['{', '&']: func = self._make_get_literal(name) - elif matches['tag'] == '': + elif tag_type == '': func = self._make_get_escaped(name) - elif matches['tag'] == '/': + elif tag_type == '/': # TODO: don't use exceptions for flow control. raise EndOfSection(parse_tree, template[start_index:match_index], end_index) else: - raise Exception("'%s' is an unrecognized type!" % matches['tag']) + raise Exception("Unrecognized tag type: %s" % repr(tag_type)) parse_tree.append(func) -- cgit v1.2.1 From 19ac543d60ea2cd59a65e817adc0ba076e799f6a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 00:57:54 -0800 Subject: Created _Parser class inside renderengine.py. --- pystache/renderengine.py | 155 ++++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index fecd937..6183038 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -110,12 +110,9 @@ class RenderEngine(object): """ - _delimiters = None - _template_re = None - nonblank_re = re.compile(r'^(.)', re.M) - def __init__(self, load_partial=None, literal=None, escape=None, delimiters=None): + def __init__(self, load_partial=None, literal=None, escape=None): """ Arguments: @@ -145,64 +142,10 @@ class RenderEngine(object): from plain unicode strings. """ - if delimiters is None: - delimiters = DEFAULT_DELIMITERS - - self._delimiters = delimiters self.escape = escape self.literal = literal self.load_partial = load_partial - def _compile_template_re(self): - self._template_re = _compile_template_re(self._delimiters) - - def _change_delimiters(self, delimiters): - self._delimiters = delimiters - self._compile_template_re() - - def render(self, template, context): - """ - Return a template rendered as a string with type unicode. - - Arguments: - - template: a template string of type unicode (but not a proper - subclass of unicode). - - context: a Context instance. - - """ - # Be strict but not too strict. In other words, accept str instead - # of unicode, but don't assume anything about the encoding (e.g. - # don't use self.literal). - template = unicode(template) - - return self._render_template(template=template, context=context) - - def _render_template(self, template, context): - """ - Returns: a string of type unicode. - - Arguments: - - template: template string - context: a Context instance - - """ - if type(template) is not unicode: - raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - - parse_tree = self.parse_string_to_tree(template_string=template) - return render_parse_tree(parse_tree, context) - - def parse_string_to_tree(self, template_string, delims=None): - - engine = RenderEngine(load_partial=self.load_partial, - literal=self.literal, escape=self.escape, delimiters=delims) - engine._compile_template_re() - - return engine.parse_to_tree(template=template_string) - def _get_string_value(self, context, tag_name): """ Get a value from the given context as a basestring instance. @@ -233,7 +176,7 @@ class RenderEngine(object): template = str(template) if type(template) is not unicode: template = self.literal(template) - val = self._render_template(template, context) + val = self._render(template, context) if not isinstance(val, basestring): val = str(val) @@ -275,7 +218,7 @@ class RenderEngine(object): template = self.load_partial(name) # Indent before rendering. template = re.sub(self.nonblank_re, indentation + r'\1', template) - return self._render_template(template, context) + return self._render(template, context) return get_partial @@ -293,7 +236,7 @@ class RenderEngine(object): elif callable(data): # TODO: should we check the arity? template = data(template) - parse_tree = self.parse_string_to_tree(template_string=template, delims=delims) + parse_tree = self._parse_to_tree(template_string=template, delimiters=delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -308,7 +251,77 @@ class RenderEngine(object): return get_section - def parse_to_tree(self, template, index=0): + def _parse_to_tree(self, template_string, delimiters=None): + """ + Parse the given template into a parse tree using a new parser. + + """ + parser = _Parser(self, delimiters=delimiters) + parser.compile_template_re() + + return parser.parse(template=template_string) + + def _render(self, template, context): + """ + Returns: a string of type unicode. + + Arguments: + + template: template string + context: a Context instance + + """ + if type(template) is not unicode: + raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) + + parse_tree = self._parse_to_tree(template_string=template) + + return render_parse_tree(parse_tree, context) + + def render(self, template, context): + """ + Return a template rendered as a string with type unicode. + + Arguments: + + template: a template string of type unicode (but not a proper + subclass of unicode). + + context: a Context instance. + + """ + # Be strict but not too strict. In other words, accept str instead + # of unicode, but don't assume anything about the encoding (e.g. + # don't use self.literal). + template = unicode(template) + + return self._render(template, context) + + +class _Parser(object): + + _delimiters = None + _template_re = None + + def __init__(self, engine, delimiters=None): + """ + Construct an instance. + + """ + if delimiters is None: + delimiters = DEFAULT_DELIMITERS + + self._delimiters = delimiters + self.engine = engine + + def compile_template_re(self): + self._template_re = _compile_template_re(self._delimiters) + + def _change_delimiters(self, delimiters): + self._delimiters = delimiters + self.compile_template_re() + + def parse(self, template, index=0): """ Parse a template string into a syntax tree using current attributes. @@ -343,11 +356,13 @@ class RenderEngine(object): def _handle_match(self, template, parse_tree, matches, start_index, match_index, end_index): + engine = self.engine + # Normalize the matches dictionary. if matches['change'] is not None: matches.update(tag='=', name=matches['delims']) elif matches['raw'] is not None: - matches.update(tag='{', name=matches['raw_name']) + matches.update(tag='&', name=matches['raw_name']) tag_type = matches['tag'] @@ -355,7 +370,7 @@ class RenderEngine(object): # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS - is_tag_interpolating = tag_type in ['', '&', '{'] + is_tag_interpolating = tag_type in ['', '&'] if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: if end_index < len(template): @@ -378,28 +393,28 @@ class RenderEngine(object): return end_index if tag_type == '>': - func = self._make_get_partial(name, matches['whitespace']) + func = engine._make_get_partial(name, matches['whitespace']) elif tag_type in ['#', '^']: try: - self.parse_to_tree(template=template, index=end_index) + self.parse(template=template, index=end_index) except EndOfSection as e: bufr = e.parse_tree tmpl = e.template end_index = e.position if tag_type == '#': - func = self._make_get_section(name, bufr, tmpl, self._delimiters) + func = engine._make_get_section(name, bufr, tmpl, self._delimiters) else: func = _make_get_inverse(name, bufr) - elif tag_type in ['{', '&']: + elif tag_type == '&': - func = self._make_get_literal(name) + func = engine._make_get_literal(name) elif tag_type == '': - func = self._make_get_escaped(name) + func = engine._make_get_escaped(name) elif tag_type == '/': -- cgit v1.2.1 From 854b84c94fbd2e7082ffa1e44f79f8ed1082b92b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 01:19:10 -0800 Subject: Created a parser module. --- pystache/parser.py | 182 ++++++++++++++++++++++++++++++++++++++++++ pystache/renderengine.py | 200 ++++------------------------------------------- 2 files changed, 199 insertions(+), 183 deletions(-) create mode 100644 pystache/parser.py diff --git a/pystache/parser.py b/pystache/parser.py new file mode 100644 index 0000000..d1a03b8 --- /dev/null +++ b/pystache/parser.py @@ -0,0 +1,182 @@ +# coding: utf-8 + +""" +Provides a class for parsing template strings. + +This module is only meant for internal use by the renderengine module. + +""" + +import re + + +DEFAULT_DELIMITERS = ('{{', '}}') +END_OF_LINE_CHARACTERS = ['\r', '\n'] + + +def _compile_template_re(delimiters): + + # The possible tag type characters following the opening tag, + # excluding "=" and "{". + tag_types = "!>&/#^" + + # TODO: are we following this in the spec? + # + # The tag's content MUST be a non-whitespace character sequence + # NOT containing the current closing delimiter. + # + tag = r""" + (?P[\ \t]*) + %(otag)s \s* + (?: + (?P=) \s* (?P.+?) \s* = | + (?P{) \s* (?P.+?) \s* } | + (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) + ) + \s* %(ctag)s + """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} + + return re.compile(tag, re.VERBOSE) + + +class EndOfSection(Exception): + def __init__(self, parse_tree, template, position): + self.parse_tree = parse_tree + self.template = template + self.position = position + + +class Parser(object): + + _delimiters = None + _template_re = None + + def __init__(self, engine, delimiters=None): + """ + Construct an instance. + + Arguments: + + engine: a RenderEngine instance. + + """ + if delimiters is None: + delimiters = DEFAULT_DELIMITERS + + self._delimiters = delimiters + self.engine = engine + + def compile_template_re(self): + self._template_re = _compile_template_re(self._delimiters) + + def _change_delimiters(self, delimiters): + self._delimiters = delimiters + self.compile_template_re() + + def parse(self, template, index=0): + """ + Parse a template string into a syntax tree using current attributes. + + This method uses the current RenderEngine instance's attributes, + including the current tag delimiter, etc. + + """ + parse_tree = [] + start_index = index + + while True: + match = self._template_re.search(template, index) + + if match is None: + break + + match_index = match.start() + end_index = match.end() + + before_tag = template[index : match_index] + + parse_tree.append(before_tag) + + matches = match.groupdict() + + index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) + + # Save the rest of the template. + parse_tree.append(template[index:]) + + return parse_tree + + def _handle_match(self, template, parse_tree, matches, start_index, match_index, end_index): + + engine = self.engine + + # Normalize the matches dictionary. + if matches['change'] is not None: + matches.update(tag='=', name=matches['delims']) + elif matches['raw'] is not None: + matches.update(tag='&', name=matches['raw_name']) + + tag_type = matches['tag'] + + # Standalone (non-interpolation) tags consume the entire line, + # both leading whitespace and trailing newline. + did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS + did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS + is_tag_interpolating = tag_type in ['', '&'] + + if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: + if end_index < len(template): + end_index += template[end_index] == '\r' and 1 or 0 + if end_index < len(template): + end_index += template[end_index] == '\n' and 1 or 0 + elif matches['whitespace']: + parse_tree.append(matches['whitespace']) + match_index += len(matches['whitespace']) + matches['whitespace'] = '' + + name = matches['name'] + + if tag_type == '!': + return end_index + + if tag_type == '=': + delimiters = name.split() + self._change_delimiters(delimiters) + return end_index + + if tag_type == '>': + func = engine._make_get_partial(name, matches['whitespace']) + elif tag_type in ['#', '^']: + + try: + self.parse(template=template, index=end_index) + except EndOfSection as e: + bufr = e.parse_tree + tmpl = e.template + end_index = e.position + + if tag_type == '#': + func = engine._make_get_section(name, bufr, tmpl, self._delimiters) + else: + func = engine._make_get_inverse(name, bufr) + + elif tag_type == '&': + + func = engine._make_get_literal(name) + + elif tag_type == '': + + func = engine._make_get_escaped(name) + + elif tag_type == '/': + + # TODO: don't use exceptions for flow control. + raise EndOfSection(parse_tree, template[start_index:match_index], end_index) + + else: + raise Exception("Unrecognized tag type: %s" % repr(tag_type)) + + parse_tree.append(func) + + return end_index + diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 6183038..68e388b 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -7,34 +7,10 @@ Defines a class responsible for rendering logic. import re +from parser import Parser -DEFAULT_DELIMITERS = ('{{', '}}') -END_OF_LINE_CHARACTERS = ['\r', '\n'] - -def _compile_template_re(delimiters): - - # The possible tag type characters following the opening tag, - # excluding "=" and "{". - tag_types = "!>&/#^" - - # TODO: are we following this in the spec? - # - # The tag's content MUST be a non-whitespace character sequence - # NOT containing the current closing delimiter. - # - tag = r""" - (?P[\ \t]*) - %(otag)s \s* - (?: - (?P=) \s* (?P.+?) \s* = | - (?P{) \s* (?P.+?) \s* } | - (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) - ) - \s* %(ctag)s - """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} - - return re.compile(tag, re.VERBOSE) +NON_BLANK_RE = re.compile(r'^(.)', re.M) def render_parse_tree(parse_tree, context): @@ -70,27 +46,6 @@ def render_parse_tree(parse_tree, context): return unicode(s) -def _make_get_inverse(name, parsed): - def get_inverse(context): - """ - Returns a string with type unicode. - - """ - data = context.get(name) - if data: - return u'' - return render_parse_tree(parsed, context) - - return get_inverse - - -class EndOfSection(Exception): - def __init__(self, parse_tree, template, position): - self.parse_tree = parse_tree - self.template = template - self.position = position - - class RenderEngine(object): """ @@ -110,8 +65,6 @@ class RenderEngine(object): """ - nonblank_re = re.compile(r'^(.)', re.M) - def __init__(self, load_partial=None, literal=None, escape=None): """ Arguments: @@ -217,11 +170,24 @@ class RenderEngine(object): """ template = self.load_partial(name) # Indent before rendering. - template = re.sub(self.nonblank_re, indentation + r'\1', template) + template = re.sub(NON_BLANK_RE, indentation + r'\1', template) return self._render(template, context) return get_partial + def _make_get_inverse(self, name, parsed): + def get_inverse(context): + """ + Returns a string with type unicode. + + """ + data = context.get(name) + if data: + return u'' + return render_parse_tree(parsed, context) + + return get_inverse + def _make_get_section(self, name, parse_tree_, template_, delims): def get_section(context): """ @@ -256,7 +222,7 @@ class RenderEngine(object): Parse the given template into a parse tree using a new parser. """ - parser = _Parser(self, delimiters=delimiters) + parser = Parser(self, delimiters=delimiters) parser.compile_template_re() return parser.parse(template=template_string) @@ -296,135 +262,3 @@ class RenderEngine(object): template = unicode(template) return self._render(template, context) - - -class _Parser(object): - - _delimiters = None - _template_re = None - - def __init__(self, engine, delimiters=None): - """ - Construct an instance. - - """ - if delimiters is None: - delimiters = DEFAULT_DELIMITERS - - self._delimiters = delimiters - self.engine = engine - - def compile_template_re(self): - self._template_re = _compile_template_re(self._delimiters) - - def _change_delimiters(self, delimiters): - self._delimiters = delimiters - self.compile_template_re() - - def parse(self, template, index=0): - """ - Parse a template string into a syntax tree using current attributes. - - This method uses the current RenderEngine instance's attributes, - including the current tag delimiter, etc. - - """ - parse_tree = [] - start_index = index - - while True: - match = self._template_re.search(template, index) - - if match is None: - break - - match_index = match.start() - end_index = match.end() - - before_tag = template[index : match_index] - - parse_tree.append(before_tag) - - matches = match.groupdict() - - index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) - - # Save the rest of the template. - parse_tree.append(template[index:]) - - return parse_tree - - def _handle_match(self, template, parse_tree, matches, start_index, match_index, end_index): - - engine = self.engine - - # Normalize the matches dictionary. - if matches['change'] is not None: - matches.update(tag='=', name=matches['delims']) - elif matches['raw'] is not None: - matches.update(tag='&', name=matches['raw_name']) - - tag_type = matches['tag'] - - # Standalone (non-interpolation) tags consume the entire line, - # both leading whitespace and trailing newline. - did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS - did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS - is_tag_interpolating = tag_type in ['', '&'] - - if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: - if end_index < len(template): - end_index += template[end_index] == '\r' and 1 or 0 - if end_index < len(template): - end_index += template[end_index] == '\n' and 1 or 0 - elif matches['whitespace']: - parse_tree.append(matches['whitespace']) - match_index += len(matches['whitespace']) - matches['whitespace'] = '' - - name = matches['name'] - - if tag_type == '!': - return end_index - - if tag_type == '=': - delimiters = name.split() - self._change_delimiters(delimiters) - return end_index - - if tag_type == '>': - func = engine._make_get_partial(name, matches['whitespace']) - elif tag_type in ['#', '^']: - - try: - self.parse(template=template, index=end_index) - except EndOfSection as e: - bufr = e.parse_tree - tmpl = e.template - end_index = e.position - - if tag_type == '#': - func = engine._make_get_section(name, bufr, tmpl, self._delimiters) - else: - func = _make_get_inverse(name, bufr) - - elif tag_type == '&': - - func = engine._make_get_literal(name) - - elif tag_type == '': - - func = engine._make_get_escaped(name) - - elif tag_type == '/': - - # TODO: don't use exceptions for flow control. - raise EndOfSection(parse_tree, template[start_index:match_index], end_index) - - else: - raise Exception("Unrecognized tag type: %s" % repr(tag_type)) - - parse_tree.append(func) - - return end_index - -- cgit v1.2.1 From a3141ab86dcbae635a4a36c1805c854b42bc8519 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 11:25:55 -0800 Subject: Changed Parser.parse() to read from the matches dictionary earlier. --- pystache/parser.py | 75 +++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index d1a03b8..53232b5 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -31,7 +31,7 @@ def _compile_template_re(delimiters): (?: (?P=) \s* (?P.+?) \s* = | (?P{) \s* (?P.+?) \s* } | - (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) + (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) ) \s* %(ctag)s """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])} @@ -99,53 +99,53 @@ class Parser(object): matches = match.groupdict() - index = self._handle_match(template, parse_tree, matches, start_index, match_index, end_index) + # Normalize the matches dictionary. + if matches['change'] is not None: + matches.update(tag='=', tag_key=matches['delims']) + elif matches['raw'] is not None: + matches.update(tag='&', tag_key=matches['raw_name']) + + tag_type = matches['tag'] + tag_key = matches['tag_key'] + leading_whitespace = matches['whitespace'] + + # Standalone (non-interpolation) tags consume the entire line, + # both leading whitespace and trailing newline. + did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS + did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS + is_tag_interpolating = tag_type in ['', '&'] + + if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: + if end_index < len(template): + end_index += template[end_index] == '\r' and 1 or 0 + if end_index < len(template): + end_index += template[end_index] == '\n' and 1 or 0 + elif leading_whitespace: + parse_tree.append(leading_whitespace) + match_index += len(leading_whitespace) + leading_whitespace = '' + + index = self._handle_match(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) return parse_tree - def _handle_match(self, template, parse_tree, matches, start_index, match_index, end_index): - - engine = self.engine - - # Normalize the matches dictionary. - if matches['change'] is not None: - matches.update(tag='=', name=matches['delims']) - elif matches['raw'] is not None: - matches.update(tag='&', name=matches['raw_name']) - - tag_type = matches['tag'] - - # Standalone (non-interpolation) tags consume the entire line, - # both leading whitespace and trailing newline. - did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS - did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS - is_tag_interpolating = tag_type in ['', '&'] - - if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: - if end_index < len(template): - end_index += template[end_index] == '\r' and 1 or 0 - if end_index < len(template): - end_index += template[end_index] == '\n' and 1 or 0 - elif matches['whitespace']: - parse_tree.append(matches['whitespace']) - match_index += len(matches['whitespace']) - matches['whitespace'] = '' - - name = matches['name'] + def _handle_match(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): if tag_type == '!': return end_index if tag_type == '=': - delimiters = name.split() + delimiters = tag_key.split() self._change_delimiters(delimiters) return end_index + engine = self.engine + if tag_type == '>': - func = engine._make_get_partial(name, matches['whitespace']) + func = engine._make_get_partial(tag_key, leading_whitespace) elif tag_type in ['#', '^']: try: @@ -156,20 +156,21 @@ class Parser(object): end_index = e.position if tag_type == '#': - func = engine._make_get_section(name, bufr, tmpl, self._delimiters) + func = engine._make_get_section(tag_key, bufr, tmpl, self._delimiters) else: - func = engine._make_get_inverse(name, bufr) + func = engine._make_get_inverse(tag_key, bufr) elif tag_type == '&': - func = engine._make_get_literal(name) + func = engine._make_get_literal(tag_key) elif tag_type == '': - func = engine._make_get_escaped(name) + func = engine._make_get_escaped(tag_key) elif tag_type == '/': + # TODO: check that tag key matches section start tag key. # TODO: don't use exceptions for flow control. raise EndOfSection(parse_tree, template[start_index:match_index], end_index) -- cgit v1.2.1 From d688087016446c08316ebde79a0535de18df62ca Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 11:35:34 -0800 Subject: Created _parse_section() method. --- pystache/parser.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 53232b5..be9a8d5 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -40,10 +40,11 @@ def _compile_template_re(delimiters): class EndOfSection(Exception): - def __init__(self, parse_tree, template, position): + + def __init__(self, parse_tree, template, index_end): self.parse_tree = parse_tree self.template = template - self.position = position + self.index_end = index_end class Parser(object): @@ -132,6 +133,16 @@ class Parser(object): return parse_tree + def _parse_section(self, template, index_start): + try: + self.parse(template=template, index=index_start) + except EndOfSection as err: + buff = err.parse_tree + template = err.template + index_end = err.index_end + + return buff, template, index_end + def _handle_match(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): if tag_type == '!': @@ -148,17 +159,12 @@ class Parser(object): func = engine._make_get_partial(tag_key, leading_whitespace) elif tag_type in ['#', '^']: - try: - self.parse(template=template, index=end_index) - except EndOfSection as e: - bufr = e.parse_tree - tmpl = e.template - end_index = e.position + buff, template, end_index = self._parse_section(template, end_index) if tag_type == '#': - func = engine._make_get_section(tag_key, bufr, tmpl, self._delimiters) + func = engine._make_get_section(tag_key, buff, template, self._delimiters) else: - func = engine._make_get_inverse(tag_key, bufr) + func = engine._make_get_inverse(tag_key, buff) elif tag_type == '&': -- cgit v1.2.1 From 6240fe23b6d486a66b1b3e5311a32a58e199a04b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 11:48:27 -0800 Subject: Broke # and ^ into separate cases. --- pystache/parser.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index be9a8d5..45b1eb8 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -126,7 +126,7 @@ class Parser(object): match_index += len(leading_whitespace) leading_whitespace = '' - index = self._handle_match(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) + index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) @@ -143,7 +143,7 @@ class Parser(object): return buff, template, index_end - def _handle_match(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): + def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): if tag_type == '!': return end_index @@ -156,15 +156,18 @@ class Parser(object): engine = self.engine if tag_type == '>': + func = engine._make_get_partial(tag_key, leading_whitespace) - elif tag_type in ['#', '^']: + + elif tag_type == '#': buff, template, end_index = self._parse_section(template, end_index) + func = engine._make_get_section(tag_key, buff, template, self._delimiters) - if tag_type == '#': - func = engine._make_get_section(tag_key, buff, template, self._delimiters) - else: - func = engine._make_get_inverse(tag_key, buff) + elif tag_type == '^': + + buff, template, end_index = self._parse_section(template, end_index) + func = engine._make_get_inverse(tag_key, buff) elif tag_type == '&': -- cgit v1.2.1 From 58e427d2b5fb4d2a5041ef8a7eb70de849256a58 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 11:51:55 -0800 Subject: Reordered tag_type "switch" statement. --- pystache/parser.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 45b1eb8..4b35686 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -155,9 +155,13 @@ class Parser(object): engine = self.engine - if tag_type == '>': + if tag_type == '': - func = engine._make_get_partial(tag_key, leading_whitespace) + func = engine._make_get_escaped(tag_key) + + elif tag_type == '&': + + func = engine._make_get_literal(tag_key) elif tag_type == '#': @@ -169,13 +173,9 @@ class Parser(object): buff, template, end_index = self._parse_section(template, end_index) func = engine._make_get_inverse(tag_key, buff) - elif tag_type == '&': - - func = engine._make_get_literal(tag_key) + elif tag_type == '>': - elif tag_type == '': - - func = engine._make_get_escaped(tag_key) + func = engine._make_get_partial(tag_key, leading_whitespace) elif tag_type == '/': -- cgit v1.2.1 From 2bef71e4ae66eade9fd0c0e44b3d2f712cd32b29 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 12:05:00 -0800 Subject: Addressed TODO not to use exceptions for flow control in parser. --- pystache/parser.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 4b35686..5e3131c 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -126,6 +126,11 @@ class Parser(object): match_index += len(leading_whitespace) leading_whitespace = '' + if tag_type == '/': + + # TODO: check that tag key matches section start tag key. + return parse_tree, template[start_index:match_index], end_index + index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) # Save the rest of the template. @@ -134,14 +139,9 @@ class Parser(object): return parse_tree def _parse_section(self, template, index_start): - try: - self.parse(template=template, index=index_start) - except EndOfSection as err: - buff = err.parse_tree - template = err.template - index_end = err.index_end + parse_tree, template, index_end = self.parse(template=template, index=index_start) - return buff, template, index_end + return parse_tree, template, index_end def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): @@ -177,13 +177,8 @@ class Parser(object): func = engine._make_get_partial(tag_key, leading_whitespace) - elif tag_type == '/': - - # TODO: check that tag key matches section start tag key. - # TODO: don't use exceptions for flow control. - raise EndOfSection(parse_tree, template[start_index:match_index], end_index) - else: + raise Exception("Unrecognized tag type: %s" % repr(tag_type)) parse_tree.append(func) -- cgit v1.2.1 From 182803baab9cfece9bba7a716131236d150be3e8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 12:57:55 -0800 Subject: Added ParsingError. --- pystache/parser.py | 20 +++++++++----------- tests/test_renderengine.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 5e3131c..047fe96 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -39,12 +39,9 @@ def _compile_template_re(delimiters): return re.compile(tag, re.VERBOSE) -class EndOfSection(Exception): +class ParsingError(Exception): - def __init__(self, parse_tree, template, index_end): - self.parse_tree = parse_tree - self.template = template - self.index_end = index_end + pass class Parser(object): @@ -74,7 +71,7 @@ class Parser(object): self._delimiters = delimiters self.compile_template_re() - def parse(self, template, index=0): + def parse(self, template, index=0, section_key=None): """ Parse a template string into a syntax tree using current attributes. @@ -127,8 +124,9 @@ class Parser(object): leading_whitespace = '' if tag_type == '/': + if tag_key != section_key: + raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(section_key))) - # TODO: check that tag key matches section start tag key. return parse_tree, template[start_index:match_index], end_index index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) @@ -138,8 +136,8 @@ class Parser(object): return parse_tree - def _parse_section(self, template, index_start): - parse_tree, template, index_end = self.parse(template=template, index=index_start) + def _parse_section(self, template, index_start, section_key): + parse_tree, template, index_end = self.parse(template=template, index=index_start, section_key=section_key) return parse_tree, template, index_end @@ -165,12 +163,12 @@ class Parser(object): elif tag_type == '#': - buff, template, end_index = self._parse_section(template, end_index) + buff, template, end_index = self._parse_section(template, end_index, tag_key) func = engine._make_get_section(tag_key, buff, template, self._delimiters) elif tag_type == '^': - buff, template, end_index = self._parse_section(template, end_index) + buff, template, end_index = self._parse_section(template, end_index, tag_key) func = engine._make_get_inverse(tag_key, buff) elif tag_type == '>': diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index 8df23f8..e32b3e2 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -9,6 +9,7 @@ import cgi import unittest from pystache.context import Context +from pystache.parser import ParsingError from pystache.renderengine import RenderEngine from tests.common import assert_strings @@ -270,6 +271,28 @@ class RenderTests(unittest.TestCase): ## Test cases related specifically to sections. + def test_section__end_tag_with_no_start_tag(self): + """ + Check what happens if there is an end tag with no start tag. + + """ + template = '{{/section}}' + try: + self._assert_render(None, template) + except ParsingError, err: + self.assertEquals(str(err), "Section end tag mismatch: u'section' != None") + + def test_section__end_tag_mismatch(self): + """ + Check what happens if the end tag doesn't match. + + """ + template = '{{#section_start}}{{/section_end}}' + try: + self._assert_render(None, template) + except ParsingError, err: + self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'") + def test_section__context_values(self): """ Test that escape and literal work on context values in sections. -- cgit v1.2.1 From 5c91b4baa5a58f2982efe0799535465688928b15 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 13:09:04 -0800 Subject: Tweaked Parser.parse() docstring. --- pystache/parser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 047fe96..6351665 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -73,10 +73,9 @@ class Parser(object): def parse(self, template, index=0, section_key=None): """ - Parse a template string into a syntax tree using current attributes. + Parse a template string into a parse tree. - This method uses the current RenderEngine instance's attributes, - including the current tag delimiter, etc. + This method uses the current tag delimiter. """ parse_tree = [] -- cgit v1.2.1 From c35b3303d4fdc6b95504bc4f85689b965edf5608 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 15:17:18 -0800 Subject: Created a ParsedTemplate class. --- pystache/parser.py | 20 +++++++++-------- pystache/renderengine.py | 56 +++++++++++------------------------------------- pystache/template.py | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 pystache/template.py diff --git a/pystache/parser.py b/pystache/parser.py index 6351665..0b4d2b7 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -9,6 +9,8 @@ This module is only meant for internal use by the renderengine module. import re +from template import ParsedTemplate + DEFAULT_DELIMITERS = ('{{', '}}') END_OF_LINE_CHARACTERS = ['\r', '\n'] @@ -73,7 +75,7 @@ class Parser(object): def parse(self, template, index=0, section_key=None): """ - Parse a template string into a parse tree. + Parse a template string into a ParsedTemplate instance. This method uses the current tag delimiter. @@ -126,19 +128,19 @@ class Parser(object): if tag_key != section_key: raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(section_key))) - return parse_tree, template[start_index:match_index], end_index + return ParsedTemplate(parse_tree), template[start_index:match_index], end_index index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) # Save the rest of the template. parse_tree.append(template[index:]) - return parse_tree + return ParsedTemplate(parse_tree) def _parse_section(self, template, index_start, section_key): - parse_tree, template, index_end = self.parse(template=template, index=index_start, section_key=section_key) + parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=section_key) - return parse_tree, template, index_end + return parsed_template, template, index_end def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): @@ -162,13 +164,13 @@ class Parser(object): elif tag_type == '#': - buff, template, end_index = self._parse_section(template, end_index, tag_key) - func = engine._make_get_section(tag_key, buff, template, self._delimiters) + parsed_section, template, end_index = self._parse_section(template, end_index, tag_key) + func = engine._make_get_section(tag_key, parsed_section, template, self._delimiters) elif tag_type == '^': - buff, template, end_index = self._parse_section(template, end_index, tag_key) - func = engine._make_get_inverse(tag_key, buff) + parsed_section, template, end_index = self._parse_section(template, end_index, tag_key) + func = engine._make_get_inverse(tag_key, parsed_section) elif tag_type == '>': diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 68e388b..f2d46f8 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -13,39 +13,6 @@ from parser import Parser NON_BLANK_RE = re.compile(r'^(.)', re.M) -def render_parse_tree(parse_tree, context): - """ - Returns: a string of type unicode. - - The elements of parse_tree can be any of the following: - - * a unicode string - * the return value of a call to any of the following: - - * RenderEngine._make_get_literal(): - Args: context - Returns: unicode - * RenderEngine._make_get_escaped(): - Args: context - Returns: unicode - * RenderEngine._make_get_partial() - Args: context - Returns: unicode - * RenderEngine._make_get_section() - Args: context - Returns: unicode - * _make_get_inverse() - Args: context - Returns: unicode - - """ - get_unicode = lambda val: val(context) if callable(val) else val - parts = map(get_unicode, parse_tree) - s = ''.join(parts) - - return unicode(s) - - class RenderEngine(object): """ @@ -175,7 +142,7 @@ class RenderEngine(object): return get_partial - def _make_get_inverse(self, name, parsed): + def _make_get_inverse(self, name, parsed_template): def get_inverse(context): """ Returns a string with type unicode. @@ -184,25 +151,28 @@ class RenderEngine(object): data = context.get(name) if data: return u'' - return render_parse_tree(parsed, context) + return parsed_template.render(context) return get_inverse - def _make_get_section(self, name, parse_tree_, template_, delims): + # 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_, template_, delims): def get_section(context): """ Returns: a string of type unicode. """ template = template_ - parse_tree = parse_tree_ + parsed_template = parsed_template_ data = context.get(name) if not data: data = [] elif callable(data): # TODO: should we check the arity? template = data(template) - parse_tree = self._parse_to_tree(template_string=template, delimiters=delims) + parsed_template = self._parse(template_string=template, delimiters=delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -210,16 +180,16 @@ class RenderEngine(object): parts = [] for element in data: context.push(element) - parts.append(render_parse_tree(parse_tree, context)) + parts.append(parsed_template.render(context)) context.pop() return unicode(''.join(parts)) return get_section - def _parse_to_tree(self, template_string, delimiters=None): + def _parse(self, template_string, delimiters=None): """ - Parse the given template into a parse tree using a new parser. + Parse the given template, and return a ParsedTemplate instance. """ parser = Parser(self, delimiters=delimiters) @@ -240,9 +210,9 @@ class RenderEngine(object): if type(template) is not unicode: raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - parse_tree = self._parse_to_tree(template_string=template) + parsed_template = self._parse(template_string=template) - return render_parse_tree(parse_tree, context) + return parsed_template.render(context) def render(self, template, context): """ diff --git a/pystache/template.py b/pystache/template.py new file mode 100644 index 0000000..ac8483d --- /dev/null +++ b/pystache/template.py @@ -0,0 +1,47 @@ +# coding: utf-8 + +""" +Exposes a class that represents a parsed (or compiled) template. + +This module is meant only for internal use. + +""" + + +class ParsedTemplate(object): + + def __init__(self, parse_tree): + self._parse_tree = parse_tree + + def render(self, context): + """ + Returns: a string of type unicode. + + The elements of parse_tree can be any of the following: + + * a unicode string + * the return value of a call to any of the following: + + * RenderEngine._make_get_literal(): + Args: context + Returns: unicode + * RenderEngine._make_get_escaped(): + Args: context + Returns: unicode + * RenderEngine._make_get_partial() + Args: context + Returns: unicode + * RenderEngine._make_get_section() + Args: context + Returns: unicode + * _make_get_inverse() + Args: context + Returns: unicode + + """ + get_unicode = lambda val: val(context) if callable(val) else val + parts = map(get_unicode, self._parse_tree) + s = ''.join(parts) + + return unicode(s) + -- cgit v1.2.1 From 5189316b8517ec6ef021431c1e550e7693652b66 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 15:37:53 -0800 Subject: Removed two unused arguments from _handle_tag_type(). --- pystache/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 0b4d2b7..44bf7bd 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -130,7 +130,7 @@ class Parser(object): return ParsedTemplate(parse_tree), template[start_index:match_index], end_index - index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index) + index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index) # Save the rest of the template. parse_tree.append(template[index:]) @@ -142,7 +142,7 @@ class Parser(object): return parsed_template, template, index_end - def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, start_index, match_index, end_index): + def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index): if tag_type == '!': return end_index -- cgit v1.2.1 From cba3c79c7aae5eefd4fab4e81d6d4ac42f691957 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 14 Jan 2012 16:07:55 -0800 Subject: Renamed a RenderEngine._parse() argument from template_string to template. --- pystache/parser.py | 4 ++++ pystache/renderengine.py | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index 44bf7bd..f74d1bd 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -79,6 +79,10 @@ class Parser(object): This method uses the current tag delimiter. + Arguments: + + template: a template string of type unicode. + """ parse_tree = [] start_index = index diff --git a/pystache/renderengine.py b/pystache/renderengine.py index f2d46f8..e51d066 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -172,7 +172,7 @@ class RenderEngine(object): elif callable(data): # TODO: should we check the arity? template = data(template) - parsed_template = self._parse(template_string=template, delimiters=delims) + parsed_template = self._parse(template, delimiters=delims) data = [ data ] elif type(data) not in [list, tuple]: data = [ data ] @@ -187,15 +187,19 @@ class RenderEngine(object): return get_section - def _parse(self, template_string, delimiters=None): + def _parse(self, template, delimiters=None): """ Parse the given template, and return a ParsedTemplate instance. + Arguments: + + template: a template string of type unicode. + """ parser = Parser(self, delimiters=delimiters) parser.compile_template_re() - return parser.parse(template=template_string) + return parser.parse(template=template) def _render(self, template, context): """ @@ -203,14 +207,18 @@ class RenderEngine(object): Arguments: - template: template string - context: a Context instance + template: a template string of type unicode. + context: a Context instance. """ + # We keep this type-check as an added check because this method is + # called with template strings coming from potentially externally- + # supplied functions like self.literal, self.load_partial, etc. + # Beyond this point, we have much better control over the type. if type(template) is not unicode: raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template))) - parsed_template = self._parse(template_string=template) + parsed_template = self._parse(template) return parsed_template.render(context) -- cgit v1.2.1 From 792ce17d3fa78c5d23b9d4327c0ea404d7b1b67c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 19 Jan 2012 05:08:03 -0800 Subject: Improved a docstring in template.py. --- pystache/template.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/pystache/template.py b/pystache/template.py index ac8483d..12b5cb0 100644 --- a/pystache/template.py +++ b/pystache/template.py @@ -11,33 +11,31 @@ This module is meant only for internal use. class ParsedTemplate(object): def __init__(self, parse_tree): + """ + Arguments: + + parse_tree: a list, each element of which is either-- + + (1) a unicode string, or + (2) a "rendering" callable that accepts a Context instance + and returns a unicode string. + + The possible rendering callables are the return values of the + following functions: + + * RenderEngine._make_get_escaped() + * RenderEngine._make_get_inverse() + * RenderEngine._make_get_literal() + * RenderEngine._make_get_partial() + * RenderEngine._make_get_section() + + """ self._parse_tree = parse_tree def render(self, context): """ Returns: a string of type unicode. - The elements of parse_tree can be any of the following: - - * a unicode string - * the return value of a call to any of the following: - - * RenderEngine._make_get_literal(): - Args: context - Returns: unicode - * RenderEngine._make_get_escaped(): - Args: context - Returns: unicode - * RenderEngine._make_get_partial() - Args: context - Returns: unicode - * RenderEngine._make_get_section() - Args: context - Returns: unicode - * _make_get_inverse() - Args: context - Returns: unicode - """ get_unicode = lambda val: val(context) if callable(val) else val parts = map(get_unicode, self._parse_tree) -- cgit v1.2.1 From f26c5e7afcdf7316dc138952eab4cc5a607c1edd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 21 Jan 2012 16:12:19 -0800 Subject: Moved some of the partial logic from RenderEngine to Parser. --- pystache/parser.py | 9 ++++++++- pystache/renderengine.py | 8 +------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pystache/parser.py b/pystache/parser.py index f74d1bd..f88f61b 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -14,6 +14,7 @@ from template import ParsedTemplate DEFAULT_DELIMITERS = ('{{', '}}') END_OF_LINE_CHARACTERS = ['\r', '\n'] +NON_BLANK_RE = re.compile(r'^(.)', re.M) def _compile_template_re(delimiters): @@ -148,6 +149,7 @@ class Parser(object): def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index): + # TODO: switch to using a dictionary instead of a bunch of ifs and elifs. if tag_type == '!': return end_index @@ -178,7 +180,12 @@ class Parser(object): elif tag_type == '>': - func = engine._make_get_partial(tag_key, leading_whitespace) + template = engine.load_partial(tag_key) + + # Indent before rendering. + template = re.sub(NON_BLANK_RE, leading_whitespace + r'\1', template) + + func = engine._make_get_partial(template) else: diff --git a/pystache/renderengine.py b/pystache/renderengine.py index e51d066..3fb0ae6 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -10,9 +10,6 @@ import re from parser import Parser -NON_BLANK_RE = re.compile(r'^(.)', re.M) - - class RenderEngine(object): """ @@ -129,15 +126,12 @@ class RenderEngine(object): return get_escaped - def _make_get_partial(self, name, indentation=''): + def _make_get_partial(self, template): def get_partial(context): """ Returns: a string of type unicode. """ - template = self.load_partial(name) - # Indent before rendering. - template = re.sub(NON_BLANK_RE, indentation + r'\1', template) return self._render(template, context) return get_partial -- cgit v1.2.1 From 2976f997d82747c38333ba1322fdb9892a6f8c6d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 22 Jan 2012 13:24:13 -0800 Subject: Stubbed out view.Locator class. --- pystache/view.py | 18 ++++++++++++++++++ tests/test_view.py | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index ed97c63..4bb557c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -103,3 +103,21 @@ class View(object): def __str__(self): return self.render() + + +class Locator(object): + + """ + A class for finding the template associated to a View instance. + + """ + + def __init__(self): + pass + + def get_template(self, view): + if view.template is not None: + return view.template + + # TODO: locate template + return None diff --git a/tests/test_view.py b/tests/test_view.py index b71a5cc..ea375cb 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -6,6 +6,7 @@ from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache.view import View +from pystache.view import Locator class Thing(object): @@ -168,5 +169,16 @@ class ViewTestCase(unittest.TestCase): view = InvertedLists() self.assertEquals(view.render(), """one, two, three, empty list""") -if __name__ == '__main__': - unittest.main() + +class LocatorTests(unittest.TestCase): + + def _make_locator(self): + locator = Locator() + return locator + + def test_get_template(self): + locator = self._make_locator() + view = View() + view.template = 'foo' + + self.assertEquals(locator.get_template(view), 'foo') -- cgit v1.2.1 From 10bba6f1202a390e55653b0e200078b2d22c3e62 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 22 Jan 2012 17:50:00 -0800 Subject: Finished view.Locator.get_template() (though still need auxiliary methods). --- pystache/view.py | 35 +++++++++++++++++++++++++++++++---- tests/common.py | 8 ++++++++ tests/test_context.py | 12 +----------- tests/test_view.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 4bb557c..1ac8674 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -5,6 +5,8 @@ This module provides a View class. """ +import os.path + from .context import Context from .locator import Locator from .renderer import Renderer @@ -112,12 +114,37 @@ class Locator(object): """ - def __init__(self): - pass + def __init__(self, reader): + self.reader = reader + + # TODO: unit test + def get_relative_template_location(self, view): + """ + Return the relative template path as a (dir, file_name) pair. + + """ + if view.template_path is not None: + return os.path.split(view.template_path) + + # TODO: finish this + return None + + # TODO: unit test + def get_template_path(self, view): + """ + Return the path to the view's associated template. + + """ + if view.template_path is not None: + return os.path.split(view.template_path) + + # TODO: finish this + return None def get_template(self, view): if view.template is not None: return view.template - # TODO: locate template - return None + path = self.get_template_path(view) + + return self.reader.read(path) diff --git a/tests/common.py b/tests/common.py index 5d6cca6..467f9af 100644 --- a/tests/common.py +++ b/tests/common.py @@ -27,3 +27,11 @@ def assert_strings(test_case, actual, expected): test_case.assertEquals(actual, expected, message) +class AssertIsMixin: + + """A mixin for adding assertIs() to a unittest.TestCase.""" + + # unittest.assertIs() is not available until Python 2.7: + # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone + def assertIs(self, first, second): + self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) diff --git a/tests/test_context.py b/tests/test_context.py index 3486251..49ebbd2 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -11,7 +11,7 @@ import unittest from pystache.context import _NOT_FOUND from pystache.context import _get_value from pystache.context import Context - +from tests.common import AssertIsMixin class SimpleObject(object): @@ -39,16 +39,6 @@ class DictLike(object): return self._dict[key] -class AssertIsMixin: - - """A mixin for adding assertIs() to a unittest.TestCase.""" - - # unittest.assertIs() is not available until Python 2.7: - # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone - def assertIs(self, first, second): - self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) - - class GetValueTests(unittest.TestCase, AssertIsMixin): """Test context._get_value().""" diff --git a/tests/test_view.py b/tests/test_view.py index ea375cb..f4aff4a 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -7,6 +7,7 @@ from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache.view import View from pystache.view import Locator +from tests.common import AssertIsMixin class Thing(object): @@ -170,15 +171,59 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -class LocatorTests(unittest.TestCase): +class LocatorTests(unittest.TestCase, AssertIsMixin): def _make_locator(self): - locator = Locator() + class MockReader(object): + def read(self, path): + return "read: %s" % repr(path) + + reader = MockReader() + locator = Locator(reader=reader) return locator - def test_get_template(self): + def test_init__reader(self): + reader = "reader" # in practice, this is a reader instance. + locator = Locator(reader) + + self.assertIs(locator.reader, reader) + + # TODO: make this test real + def test_get_relative_template_location__template_path__file_name(self): + locator = self._make_locator() + view = View() + + view.template_path = 'foo.txt' + self.assertEquals(locator.get_relative_template_location(view), ('', 'foo.txt')) + + # TODO: make this test real + def test_get_relative_template_location__template_path__full_path(self): + locator = self._make_locator() + view = View() + + view.template_path = 'foo.txt' + self.assertEquals(locator.get_relative_template_location(view), ('', 'foo.txt')) + + def test_get_template__template_attribute_set(self): + """ + Test get_template() with view.template set to a non-None value. + + """ locator = self._make_locator() view = View() view.template = 'foo' self.assertEquals(locator.get_template(view), 'foo') + + def test_get_template__template_attribute_not_set(self): + """ + Test get_template() with view.template set to None. + + """ + locator = self._make_locator() + locator.get_template_path = lambda view: "path" + + view = View() + view.template = None + + self.assertEquals(locator.get_template(view), "read: 'path'") -- cgit v1.2.1 From a8464737e11d3e3a9e7f30ff5225f211b36ecb9a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 23 Jan 2012 02:51:22 -0800 Subject: Moved some locator logic from Renderer to locator.Locator.find_path_by_object(). --- pystache/locator.py | 30 ++++++++++++++++++------ pystache/renderer.py | 14 ++++------- pystache/view.py | 12 +++++++--- tests/test_locator.py | 64 ++++++++++++++++++++++++++++++++++++--------------- tests/test_view.py | 5 ++-- 5 files changed, 85 insertions(+), 40 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index 0467a76..9e4f851 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -53,6 +53,9 @@ class Locator(object): doctest that appears in a text file (rather than a Python file). """ + if not hasattr(obj, '__module__'): + return None + module = sys.modules[obj.__module__] if not hasattr(module, '__file__'): @@ -94,17 +97,30 @@ class Locator(object): return re.sub('[A-Z]', repl, template_name)[1:] - def locate_path(self, template_name, search_dirs): + def find_path(self, search_dirs, template_name): """ - Find and return the path to the template with the given name. + Return the path to a template with the given name. """ file_name = self.make_file_name(template_name) + path = self._find_path(file_name, search_dirs) - if path is not None: - return path + if path is None: + # TODO: we should probably raise an exception of our own type. + raise IOError('Template %s not found in directories: %s' % + (repr(template_name), repr(search_dirs))) + + return path + + def find_path_by_object(self, search_dirs, template_name, obj): + """ + Return the path to a template associated with the given object. + + """ + dir_path = self.get_object_directory(obj) + + if dir_path is not None: + search_dirs = [dir_path] + search_dirs - # TODO: we should probably raise an exception of our own type. - raise IOError('Template %s not found in directories: %s' % - (repr(template_name), repr(search_dirs))) + return self.find_path(search_dirs, template_name) diff --git a/pystache/renderer.py b/pystache/renderer.py index 32bda26..9910db2 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -190,8 +190,8 @@ class Renderer(object): locator = self.make_locator() def load_template(template_name): - path = locator.locate_path(template_name=template_name, search_dirs=self.search_dirs) - return reader.read(path) + template_path = locator.find_path(self.search_dirs, template_name) + return reader.read(template_path) return load_template @@ -263,19 +263,13 @@ class Renderer(object): class definition. """ - search_dirs = self.search_dirs locator = self.make_locator() template_name = locator.make_template_name(obj) - directory = locator.get_object_directory(obj) - # TODO: add a unit test for the case of a None return value. - if directory is not None: - search_dirs = [directory] + self.search_dirs + template_path = locator.find_path_by_object(self.search_dirs, template_name, obj) - path = locator.locate_path(template_name=template_name, search_dirs=search_dirs) - - return self.read(path) + return self.read(template_path) def _render_string(self, template, *context, **kwargs): """ diff --git a/pystache/view.py b/pystache/view.py index 1ac8674..a0efb61 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -8,7 +8,7 @@ This module provides a View class. import os.path from .context import Context -from .locator import Locator +from .locator import Locator as TemplateLocator from .renderer import Renderer @@ -22,7 +22,7 @@ class View(object): _renderer = None - locator = Locator() + locator = TemplateLocator() def __init__(self, template=None, context=None, partials=None, **kwargs): """ @@ -114,8 +114,9 @@ class Locator(object): """ - def __init__(self, reader): + def __init__(self, reader, template_locator): self.reader = reader + self.template_locator = template_locator # TODO: unit test def get_relative_template_location(self, view): @@ -135,6 +136,11 @@ class Locator(object): Return the path to the view's associated template. """ + dir_path, file_name = self.get_relative_template_location(view) + + if dir_path is None: + path = self.template_locator.find_path(search_dirs, file_name, obj=view) + if view.template_path is not None: return os.path.split(view.template_path) diff --git a/tests/test_locator.py b/tests/test_locator.py index a7f3909..ba22ab3 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -5,6 +5,7 @@ Contains locator.py unit tests. """ +from datetime import datetime import os import sys import unittest @@ -13,12 +14,10 @@ from pystache.locator import Locator from pystache.reader import Reader from .common import DATA_DIR - +from data.templates import SayHello class LocatorTests(unittest.TestCase): - search_dirs = 'examples' - def _locator(self): return Locator(search_dirs=DATA_DIR) @@ -36,12 +35,20 @@ class LocatorTests(unittest.TestCase): def test_get_object_directory(self): locator = Locator() - reader = Reader() - actual = locator.get_object_directory(reader) + obj = SayHello() + actual = locator.get_object_directory(obj) + + self.assertEquals(actual, os.path.abspath(DATA_DIR)) + + def test_get_object_directory__not_hasattr_module(self): + locator = Locator() - expected = os.path.join(os.path.dirname(__file__), os.pardir, 'pystache') + obj = datetime(2000, 1, 1) + self.assertFalse(hasattr(obj, '__module__')) + self.assertEquals(locator.get_object_directory(obj), None) - self.assertEquals(os.path.normpath(actual), os.path.normpath(expected)) + self.assertFalse(hasattr(None, '__module__')) + self.assertEquals(locator.get_object_directory(None), None) def test_make_file_name(self): locator = Locator() @@ -55,21 +62,21 @@ class LocatorTests(unittest.TestCase): locator.template_extension = '' self.assertEquals(locator.make_file_name('foo'), 'foo.') - def test_locate_path(self): + def test_find_path(self): locator = Locator() - path = locator.locate_path('simple', search_dirs=['examples']) + path = locator.find_path(search_dirs=['examples'], template_name='simple') self.assertEquals(os.path.basename(path), 'simple.mustache') - def test_locate_path__using_list_of_paths(self): + def test_find_path__using_list_of_paths(self): locator = Locator() - path = locator.locate_path('simple', search_dirs=['doesnt_exist', 'examples']) + path = locator.find_path(search_dirs=['doesnt_exist', 'examples'], template_name='simple') self.assertTrue(path) - def test_locate_path__precedence(self): + def test_find_path__precedence(self): """ - Test the order in which locate_path() searches directories. + Test the order in which find_path() searches directories. """ locator = Locator() @@ -77,19 +84,40 @@ class LocatorTests(unittest.TestCase): dir1 = DATA_DIR dir2 = os.path.join(DATA_DIR, 'locator') - self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir1])) - self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir2])) + self.assertTrue(locator.find_path(search_dirs=[dir1], template_name='duplicate')) + self.assertTrue(locator.find_path(search_dirs=[dir2], template_name='duplicate')) - path = locator.locate_path('duplicate', search_dirs=[dir2, dir1]) + path = locator.find_path(search_dirs=[dir2, dir1], template_name='duplicate') dirpath = os.path.dirname(path) dirname = os.path.split(dirpath)[-1] self.assertEquals(dirname, 'locator') - def test_locate_path__non_existent_template_fails(self): + def test_find_path__non_existent_template_fails(self): + locator = Locator() + + self.assertRaises(IOError, locator.find_path, search_dirs=[], template_name='doesnt_exist') + + def test_find_path_by_object(self): locator = Locator() - self.assertRaises(IOError, locator.locate_path, 'doesnt_exist', search_dirs=[]) + obj = SayHello() + + actual = locator.find_path_by_object(search_dirs=[], template_name='say_hello', obj=obj) + expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache')) + + self.assertEquals(actual, expected) + + def test_find_path_by_object__none_object_directory(self): + locator = Locator() + + obj = None + self.assertEquals(None, locator.get_object_directory(obj)) + + actual = locator.find_path_by_object(search_dirs=[DATA_DIR], template_name='say_hello', obj=obj) + expected = os.path.join(DATA_DIR, 'say_hello.mustache') + + self.assertEquals(actual, expected) def test_make_template_name(self): """ diff --git a/tests/test_view.py b/tests/test_view.py index f4aff4a..9a8401f 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -179,12 +179,13 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): return "read: %s" % repr(path) reader = MockReader() - locator = Locator(reader=reader) + # TODO: include a locator? + locator = Locator(reader=reader, template_locator=None) return locator def test_init__reader(self): reader = "reader" # in practice, this is a reader instance. - locator = Locator(reader) + locator = Locator(reader, template_locator=None) self.assertIs(locator.reader, reader) -- cgit v1.2.1 From 9202ce2353863a3b27366605c6f7017a5ab93cb6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 23 Jan 2012 06:18:26 -0800 Subject: Added locator.Locator.find_path_by_object(). Also, more progress on view.Locator.get_template_path(). --- pystache/locator.py | 30 +++++++++++++++++++------- pystache/renderer.py | 8 +++---- pystache/view.py | 20 ++++++++++-------- tests/data/sample_view.mustache | 1 + tests/data/templates.py | 7 ------ tests/data/views.py | 12 +++++++++++ tests/test_locator.py | 39 ++++++++++++++++++++++------------ tests/test_renderer.py | 2 +- tests/test_view.py | 47 +++++++++++++++++++++++++++++++++++------ 9 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 tests/data/sample_view.mustache delete mode 100644 tests/data/templates.py create mode 100644 tests/data/views.py diff --git a/pystache/locator.py b/pystache/locator.py index 9e4f851..a47e8d9 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -97,30 +97,44 @@ class Locator(object): return re.sub('[A-Z]', repl, template_name)[1:] - def find_path(self, search_dirs, template_name): + def _find_path_by_file_name(self, search_dirs, file_name): """ - Return the path to a template with the given name. + Return the path to a template with the given file name. """ - file_name = self.make_file_name(template_name) - path = self._find_path(file_name, search_dirs) if path is None: # TODO: we should probably raise an exception of our own type. - raise IOError('Template %s not found in directories: %s' % - (repr(template_name), repr(search_dirs))) + raise IOError('Template file %s not found in directories: %s' % + (repr(file_name), repr(search_dirs))) return path - def find_path_by_object(self, search_dirs, template_name, obj): + def find_path_by_name(self, search_dirs, template_name): + """ + Return the path to a template with the given name. + + """ + file_name = self.make_file_name(template_name) + + return self._find_path_by_file_name(search_dirs, file_name) + + def find_path_by_object(self, search_dirs, obj, file_name=None): """ Return the path to a template associated with the given object. """ + if file_name is None: + # TODO: should we define a make_file_name() method? + template_name = self.make_template_name(obj) + file_name = self.make_file_name(template_name) + dir_path = self.get_object_directory(obj) if dir_path is not None: search_dirs = [dir_path] + search_dirs - return self.find_path(search_dirs, template_name) + path = self._find_path_by_file_name(search_dirs, file_name) + + return path diff --git a/pystache/renderer.py b/pystache/renderer.py index 9910db2..c564162 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -190,7 +190,8 @@ class Renderer(object): locator = self.make_locator() def load_template(template_name): - template_path = locator.find_path(self.search_dirs, template_name) + template_path = locator.find_path_by_name(self.search_dirs, template_name) + return reader.read(template_path) return load_template @@ -264,10 +265,7 @@ class Renderer(object): """ locator = self.make_locator() - - template_name = locator.make_template_name(obj) - - template_path = locator.find_path_by_object(self.search_dirs, template_name, obj) + template_path = locator.find_path_by_object(self.search_dirs, obj) return self.read(template_path) diff --git a/pystache/view.py b/pystache/view.py index a0efb61..16fd246 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -12,6 +12,7 @@ from .locator import Locator as TemplateLocator from .renderer import Renderer +# TODO: rename this class to something else (e.g. ITemplateInfo) class View(object): template_name = None @@ -114,8 +115,9 @@ class Locator(object): """ - def __init__(self, reader, template_locator): + def __init__(self, reader, search_dirs, template_locator): self.reader = reader + self.search_dirs = search_dirs self.template_locator = template_locator # TODO: unit test @@ -127,10 +129,10 @@ class Locator(object): if view.template_path is not None: return os.path.split(view.template_path) + # TODO: finish this - return None + return (None, None) - # TODO: unit test def get_template_path(self, view): """ Return the path to the view's associated template. @@ -139,13 +141,13 @@ class Locator(object): dir_path, file_name = self.get_relative_template_location(view) if dir_path is None: - path = self.template_locator.find_path(search_dirs, file_name, obj=view) - - if view.template_path is not None: - return os.path.split(view.template_path) + # Then we need to search for the path. + path = self.template_locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + else: + obj_dir = self.template_locator.get_object_directory(view) + path = os.path.join(obj_dir, dir_path, file_name) - # TODO: finish this - return None + return path def get_template(self, view): if view.template is not None: diff --git a/tests/data/sample_view.mustache b/tests/data/sample_view.mustache new file mode 100644 index 0000000..3829998 --- /dev/null +++ b/tests/data/sample_view.mustache @@ -0,0 +1 @@ +Sample view... \ No newline at end of file diff --git a/tests/data/templates.py b/tests/data/templates.py deleted file mode 100644 index ef629b9..0000000 --- a/tests/data/templates.py +++ /dev/null @@ -1,7 +0,0 @@ -# coding: utf-8 - - -class SayHello(object): - - def to(self): - return "World" diff --git a/tests/data/views.py b/tests/data/views.py new file mode 100644 index 0000000..597f854 --- /dev/null +++ b/tests/data/views.py @@ -0,0 +1,12 @@ +# coding: utf-8 + +from pystache.view import View + +class SayHello(object): + + def to(self): + return "World" + + +class SampleView(View): + pass diff --git a/tests/test_locator.py b/tests/test_locator.py index ba22ab3..57dbac0 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -14,7 +14,8 @@ from pystache.locator import Locator from pystache.reader import Reader from .common import DATA_DIR -from data.templates import SayHello +from data.views import SayHello + class LocatorTests(unittest.TestCase): @@ -62,21 +63,21 @@ class LocatorTests(unittest.TestCase): locator.template_extension = '' self.assertEquals(locator.make_file_name('foo'), 'foo.') - def test_find_path(self): + def test_find_path_by_name(self): locator = Locator() - path = locator.find_path(search_dirs=['examples'], template_name='simple') + path = locator.find_path_by_name(search_dirs=['examples'], template_name='simple') self.assertEquals(os.path.basename(path), 'simple.mustache') - def test_find_path__using_list_of_paths(self): + def test_find_path_by_name__using_list_of_paths(self): locator = Locator() - path = locator.find_path(search_dirs=['doesnt_exist', 'examples'], template_name='simple') + path = locator.find_path_by_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple') self.assertTrue(path) - def test_find_path__precedence(self): + def test_find_path_by_name__precedence(self): """ - Test the order in which find_path() searches directories. + Test the order in which find_path_by_name() searches directories. """ locator = Locator() @@ -84,26 +85,36 @@ class LocatorTests(unittest.TestCase): dir1 = DATA_DIR dir2 = os.path.join(DATA_DIR, 'locator') - self.assertTrue(locator.find_path(search_dirs=[dir1], template_name='duplicate')) - self.assertTrue(locator.find_path(search_dirs=[dir2], template_name='duplicate')) + self.assertTrue(locator.find_path_by_name(search_dirs=[dir1], template_name='duplicate')) + self.assertTrue(locator.find_path_by_name(search_dirs=[dir2], template_name='duplicate')) - path = locator.find_path(search_dirs=[dir2, dir1], template_name='duplicate') + path = locator.find_path_by_name(search_dirs=[dir2, dir1], template_name='duplicate') dirpath = os.path.dirname(path) dirname = os.path.split(dirpath)[-1] self.assertEquals(dirname, 'locator') - def test_find_path__non_existent_template_fails(self): + def test_find_path_by_name__non_existent_template_fails(self): locator = Locator() - self.assertRaises(IOError, locator.find_path, search_dirs=[], template_name='doesnt_exist') + self.assertRaises(IOError, locator.find_path_by_name, search_dirs=[], template_name='doesnt_exist') def test_find_path_by_object(self): locator = Locator() obj = SayHello() - actual = locator.find_path_by_object(search_dirs=[], template_name='say_hello', obj=obj) + actual = locator.find_path_by_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') + expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) + + self.assertEquals(actual, expected) + + def test_find_path_by_object__none_file_name(self): + locator = Locator() + + obj = SayHello() + + actual = locator.find_path_by_object(search_dirs=[], obj=obj) expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache')) self.assertEquals(actual, expected) @@ -114,7 +125,7 @@ class LocatorTests(unittest.TestCase): obj = None self.assertEquals(None, locator.get_object_directory(obj)) - actual = locator.find_path_by_object(search_dirs=[DATA_DIR], template_name='say_hello', obj=obj) + actual = locator.find_path_by_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') expected = os.path.join(DATA_DIR, 'say_hello.mustache') self.assertEquals(actual, expected) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 6a58573..6172d66 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -16,7 +16,7 @@ from pystache.renderer import Renderer from pystache.locator import Locator from .common import get_data_path -from .data.templates import SayHello +from .data.views import SayHello class RendererInitTestCase(unittest.TestCase): diff --git a/tests/test_view.py b/tests/test_view.py index 9a8401f..325967a 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -1,13 +1,16 @@ +import os.path import unittest -import pystache from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists +from pystache.locator import Locator as TemplateLocator from pystache.view import View -from pystache.view import Locator -from tests.common import AssertIsMixin +from pystache.view import Locator as ViewLocator +from .common import AssertIsMixin +from .common import DATA_DIR +from .data.views import SampleView class Thing(object): @@ -179,13 +182,14 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): return "read: %s" % repr(path) reader = MockReader() - # TODO: include a locator? - locator = Locator(reader=reader, template_locator=None) + template_locator = TemplateLocator() + locator = ViewLocator(reader=reader, search_dirs=[DATA_DIR], template_locator=template_locator) return locator + # TODO: fully test constructor. def test_init__reader(self): reader = "reader" # in practice, this is a reader instance. - locator = Locator(reader, template_locator=None) + locator = ViewLocator(reader, search_dirs=None, template_locator=None) self.assertIs(locator.reader, reader) @@ -205,6 +209,37 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): view.template_path = 'foo.txt' self.assertEquals(locator.get_relative_template_location(view), ('', 'foo.txt')) + def test_get_template_path__with_directory(self): + """ + Test get_template_path() with a view that has a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + view.template_path = 'foo/bar.txt' + self.assertTrue(locator.get_relative_template_location(view)[0] is not None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) + + self.assertEquals(actual, expected) + + def test_get_template_path__without_directory(self): + """ + Test get_template_path() with a view that doesn't have a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + self.assertTrue(locator.get_relative_template_location(view)[0] is None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) + + self.assertEquals(actual, expected) + def test_get_template__template_attribute_set(self): """ Test get_template() with view.template set to a non-None value. -- cgit v1.2.1 From 3f119240f0d70500074551eb8b2dc7c6655cb2d8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 23 Jan 2012 06:30:26 -0800 Subject: Reordered and renamed some methods in locator.Locator. --- pystache/locator.py | 50 +++++++++++++++++++++++++------------------------- pystache/view.py | 4 ++++ 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index a47e8d9..247e8a2 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -30,20 +30,6 @@ class Locator(object): self.template_extension = extension - def _find_path(self, file_name, search_dirs): - """ - Search for the given file, and return the path. - - Returns None if the file is not found. - - """ - for dir_path in search_dirs: - file_path = os.path.join(dir_path, file_name) - if os.path.exists(file_path): - return file_path - - return None - def get_object_directory(self, obj): """ Return the directory containing an object's defining class. @@ -66,13 +52,6 @@ class Locator(object): return os.path.dirname(path) - def make_file_name(self, template_name): - file_name = template_name - if self.template_extension is not False: - file_name += os.path.extsep + self.template_extension - - return file_name - def make_template_name(self, obj): """ Return the canonical template name for an object instance. @@ -97,12 +76,33 @@ class Locator(object): return re.sub('[A-Z]', repl, template_name)[1:] - def _find_path_by_file_name(self, search_dirs, file_name): + def make_file_name(self, template_name): + file_name = template_name + if self.template_extension is not False: + file_name += os.path.extsep + self.template_extension + + return file_name + + def _find_path(self, search_dirs, file_name): + """ + Search for the given file, and return the path. + + Returns None if the file is not found. + + """ + for dir_path in search_dirs: + file_path = os.path.join(dir_path, file_name) + if os.path.exists(file_path): + return file_path + + return None + + def _find_path_required(self, search_dirs, file_name): """ Return the path to a template with the given file name. """ - path = self._find_path(file_name, search_dirs) + path = self._find_path(search_dirs, file_name) if path is None: # TODO: we should probably raise an exception of our own type. @@ -118,7 +118,7 @@ class Locator(object): """ file_name = self.make_file_name(template_name) - return self._find_path_by_file_name(search_dirs, file_name) + return self._find_path_required(search_dirs, file_name) def find_path_by_object(self, search_dirs, obj, file_name=None): """ @@ -135,6 +135,6 @@ class Locator(object): if dir_path is not None: search_dirs = [dir_path] + search_dirs - path = self._find_path_by_file_name(search_dirs, file_name) + path = self._find_path_required(search_dirs, file_name) return path diff --git a/pystache/view.py b/pystache/view.py index 16fd246..01b3d92 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -150,6 +150,10 @@ class Locator(object): return path def get_template(self, view): + """ + Return the unicode template string associated with a view. + + """ if view.template is not None: return view.template -- cgit v1.2.1 From 78ef793b779b8a22f3e662267a4d72ac99d7f8dd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 12:32:42 -0800 Subject: Added optional template_extension argument to Locator.make_file_name(). --- pystache/locator.py | 18 +++++++++++++++--- tests/test_locator.py | 5 +++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pystache/locator.py b/pystache/locator.py index 247e8a2..ebcbd25 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -76,10 +76,22 @@ class Locator(object): return re.sub('[A-Z]', repl, template_name)[1:] - def make_file_name(self, template_name): + def make_file_name(self, template_name, template_extension=None): + """ + Generate and return the file name for the given template name. + + Arguments: + + template_extension: defaults to the instance's extension. + + """ file_name = template_name - if self.template_extension is not False: - file_name += os.path.extsep + self.template_extension + + if template_extension is None: + template_extension = self.template_extension + + if template_extension is not False: + file_name += os.path.extsep + template_extension return file_name diff --git a/tests/test_locator.py b/tests/test_locator.py index 57dbac0..d4dcc3c 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -63,6 +63,11 @@ class LocatorTests(unittest.TestCase): locator.template_extension = '' self.assertEquals(locator.make_file_name('foo'), 'foo.') + def test_make_file_name__template_extension_argument(self): + locator = Locator() + + self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar') + def test_find_path_by_name(self): locator = Locator() path = locator.find_path_by_name(search_dirs=['examples'], template_name='simple') -- cgit v1.2.1 From 008198b5491d9c394968a8c7c3f524ddee9f59bc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 12:36:16 -0800 Subject: Added optional encoding argument to Reader.read(). --- pystache/reader.py | 7 +++++-- tests/test_reader.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pystache/reader.py b/pystache/reader.py index 4a2ec10..7477794 100644 --- a/pystache/reader.py +++ b/pystache/reader.py @@ -42,14 +42,17 @@ class Reader(object): self.decode_errors = decode_errors self.encoding = encoding - def read(self, path): + def read(self, path, encoding=None): """ Read the template at the given path, and return it as a unicode string. """ + if encoding is None: + encoding = self.encoding + with open(path, 'r') as f: text = f.read() - text = unicode(text, self.encoding, self.decode_errors) + text = unicode(text, encoding, self.decode_errors) return text diff --git a/tests/test_reader.py b/tests/test_reader.py index 1a768d4..65727db 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -55,7 +55,7 @@ class ReaderTestCase(unittest.TestCase): contents = reader.read(path) self.assertEqual(type(contents), unicode) - def test_read__encoding(self): + def test_read__encoding__attribute(self): """ Test read(): encoding attribute respected. @@ -67,6 +67,17 @@ class ReaderTestCase(unittest.TestCase): reader.encoding = 'utf-8' self.assertEquals(reader.read(path), u'non-ascii: é') + def test_read__encoding__argument(self): + """ + Test read(): encoding argument respected. + + """ + reader = Reader() + path = self._get_path('nonascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + self.assertEquals(reader.read(path, encoding='utf-8'), u'non-ascii: é') + def test_get__decode_errors(self): """ Test get(): decode_errors attribute. -- cgit v1.2.1 From 43cc3d1f55c02db6ce767ef62f24e1b1359c01c4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 13:01:04 -0800 Subject: Finished implementing and testing view.Locator.get_relative_template_location(). --- pystache/view.py | 39 ++++++++++++++++++++++++++++++++------ tests/test_view.py | 55 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 01b3d92..a19e2cf 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -15,12 +15,34 @@ from .renderer import Renderer # TODO: rename this class to something else (e.g. ITemplateInfo) class View(object): - template_name = None - template_path = None + """ + Subclass this class only if template customizations are needed. + + The following attributes allow one to customize/override template + information on a per View basis. A None value means to use default + behavior and perform no customization. All attributes are initially + set to None. + + Attributes: + + template: the template to use, as a unicode string. + + template_path: the path to the template file, relative to the + directory containing the module defining the class. + + template_extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + + """ + template = None - template_encoding = None + template_path = None + + template_name = None template_extension = None + template_encoding = None + _renderer = None locator = TemplateLocator() @@ -120,7 +142,6 @@ class Locator(object): self.search_dirs = search_dirs self.template_locator = template_locator - # TODO: unit test def get_relative_template_location(self, view): """ Return the relative template path as a (dir, file_name) pair. @@ -128,10 +149,14 @@ class Locator(object): """ if view.template_path is not None: return os.path.split(view.template_path) + # Otherwise, we don't know the directory. + + template_name = (view.template_name if view.template_name is not None else + self.template_locator.make_template_name(view)) + file_name = self.template_locator.make_file_name(template_name, view.template_extension) - # TODO: finish this - return (None, None) + return (None, file_name) def get_template_path(self, view): """ @@ -155,8 +180,10 @@ class Locator(object): """ if view.template is not None: + # TODO: unit test rendering with a non-unicode value for this attribute. return view.template path = self.get_template_path(view) + # TODO: add support for encoding. return self.reader.read(path) diff --git a/tests/test_view.py b/tests/test_view.py index 325967a..cfd5c46 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -193,21 +193,54 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): self.assertIs(locator.reader, reader) - # TODO: make this test real - def test_get_relative_template_location__template_path__file_name(self): + def _assert_template_location(self, view, expected): locator = self._make_locator() - view = View() + actual = locator.get_relative_template_location(view) + self.assertEquals(actual, expected) - view.template_path = 'foo.txt' - self.assertEquals(locator.get_relative_template_location(view), ('', 'foo.txt')) + def test_get_relative_template_location(self): + """ + Test get_relative_template_location(): default behavior (no attributes set). - # TODO: make this test real - def test_get_relative_template_location__template_path__full_path(self): - locator = self._make_locator() - view = View() + """ + view = SampleView() + self._assert_template_location(view, (None, 'sample_view.mustache')) + + def test_get_relative_template_location__template_path__file_name_only(self): + """ + Test get_relative_template_location(): template_path attribute. + + """ + view = SampleView() + view.template_path = 'template.txt' + self._assert_template_location(view, ('', 'template.txt')) + + def test_get_relative_template_location__template_path__file_name_with_directory(self): + """ + Test get_relative_template_location(): template_path attribute. + + """ + view = SampleView() + view.template_path = 'foo/bar/template.txt' + self._assert_template_location(view, ('foo/bar', 'template.txt')) - view.template_path = 'foo.txt' - self.assertEquals(locator.get_relative_template_location(view), ('', 'foo.txt')) + def test_get_relative_template_location__template_name(self): + """ + Test get_relative_template_location(): template_name attribute. + + """ + view = SampleView() + view.template_name = 'new_name' + self._assert_template_location(view, (None, 'new_name.mustache')) + + def test_get_relative_template_location__template_extension(self): + """ + Test get_relative_template_location(): template_extension attribute. + + """ + view = SampleView() + view.template_extension = 'txt' + self._assert_template_location(view, (None, 'sample_view.txt')) def test_get_template_path__with_directory(self): """ -- cgit v1.2.1 From af2b04dc0b156ded915c048c8684f6da21daebf2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:01:49 -0800 Subject: Added Reader.unicode(). --- pystache/reader.py | 13 +++++++------ tests/test_reader.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/pystache/reader.py b/pystache/reader.py index 7477794..9bb5e1d 100644 --- a/pystache/reader.py +++ b/pystache/reader.py @@ -42,17 +42,18 @@ class Reader(object): self.decode_errors = decode_errors self.encoding = encoding + def unicode(self, text, encoding=None): + if encoding is None: + encoding = self.encoding + + return unicode(text, encoding, self.decode_errors) + def read(self, path, encoding=None): """ Read the template at the given path, and return it as a unicode string. """ - if encoding is None: - encoding = self.encoding - with open(path, 'r') as f: text = f.read() - text = unicode(text, encoding, self.decode_errors) - - return text + return self.unicode(text, encoding) diff --git a/tests/test_reader.py b/tests/test_reader.py index 65727db..c9bf111 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -36,6 +36,45 @@ class ReaderTestCase(unittest.TestCase): reader = Reader(encoding='foo') self.assertEquals(reader.encoding, 'foo') + def test_unicode(self): + """ + Test unicode(): default values. + + """ + reader = Reader() + + actual = reader.unicode("foo") + + self.assertEquals(type(actual), unicode) + self.assertEquals(actual, u"foo") + + def test_unicode__encoding_attribute(self): + """ + Test unicode(): encoding attribute. + + """ + reader = Reader() + + non_ascii = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) + + reader.encoding = 'utf-8' + self.assertEquals(reader.unicode(non_ascii), u"é") + + def test_unicode__encoding_argument(self): + """ + Test unicode(): encoding argument. + + """ + reader = Reader() + + non_ascii = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) + + self.assertEquals(reader.unicode(non_ascii, encoding='utf-8'), u'é') + def test_read(self): """ Test read(). -- cgit v1.2.1 From 8782cc27e86524e5061252e9913fbb5ecb76342d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:25:50 -0800 Subject: The view.Locator class now supports view.template_encoding. --- pystache/view.py | 16 +++++++--- tests/data/non_ascii.mustache | 1 + tests/data/nonascii.mustache | 1 - tests/data/sample_view.mustache | 2 +- tests/data/views.py | 4 +++ tests/test_reader.py | 6 ++-- tests/test_renderer.py | 4 +-- tests/test_view.py | 71 +++++++++++++++++++++++++++++------------ 8 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 tests/data/non_ascii.mustache delete mode 100644 tests/data/nonascii.mustache diff --git a/pystache/view.py b/pystache/view.py index a19e2cf..1eceb77 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -9,6 +9,7 @@ import os.path from .context import Context from .locator import Locator as TemplateLocator +from .reader import Reader from .renderer import Renderer @@ -137,7 +138,14 @@ class Locator(object): """ - def __init__(self, reader, search_dirs, template_locator): + # TODO: unit test this. + def __init__(self, search_dirs, template_locator=None, reader=None): + if reader is None: + reader = Reader() + + if template_locator is None: + template_locator = TemplateLocator() + self.reader = reader self.search_dirs = search_dirs self.template_locator = template_locator @@ -180,10 +188,8 @@ class Locator(object): """ if view.template is not None: - # TODO: unit test rendering with a non-unicode value for this attribute. - return view.template + return self.reader.unicode(view.template, view.template_encoding) path = self.get_template_path(view) - # TODO: add support for encoding. - return self.reader.read(path) + return self.reader.read(path, view.template_encoding) diff --git a/tests/data/non_ascii.mustache b/tests/data/non_ascii.mustache new file mode 100644 index 0000000..bd69b61 --- /dev/null +++ b/tests/data/non_ascii.mustache @@ -0,0 +1 @@ +non-ascii: é \ No newline at end of file diff --git a/tests/data/nonascii.mustache b/tests/data/nonascii.mustache deleted file mode 100644 index bd69b61..0000000 --- a/tests/data/nonascii.mustache +++ /dev/null @@ -1 +0,0 @@ -non-ascii: é \ No newline at end of file diff --git a/tests/data/sample_view.mustache b/tests/data/sample_view.mustache index 3829998..e86737b 100644 --- a/tests/data/sample_view.mustache +++ b/tests/data/sample_view.mustache @@ -1 +1 @@ -Sample view... \ No newline at end of file +ascii: abc \ No newline at end of file diff --git a/tests/data/views.py b/tests/data/views.py index 597f854..c082abe 100644 --- a/tests/data/views.py +++ b/tests/data/views.py @@ -10,3 +10,7 @@ class SayHello(object): class SampleView(View): pass + + +class NonAscii(View): + pass diff --git a/tests/test_reader.py b/tests/test_reader.py index c9bf111..899fda1 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -100,7 +100,7 @@ class ReaderTestCase(unittest.TestCase): """ reader = Reader() - path = self._get_path('nonascii.mustache') + path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) reader.encoding = 'utf-8' @@ -112,7 +112,7 @@ class ReaderTestCase(unittest.TestCase): """ reader = Reader() - path = self._get_path('nonascii.mustache') + path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) self.assertEquals(reader.read(path, encoding='utf-8'), u'non-ascii: é') @@ -123,7 +123,7 @@ class ReaderTestCase(unittest.TestCase): """ reader = Reader() - path = self._get_path('nonascii.mustache') + path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) reader.decode_errors = 'replace' diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 6172d66..3db57eb 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -200,7 +200,7 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(type(actual), unicode) def test_read__file_encoding(self): - filename = 'nonascii.mustache' + filename = 'non_ascii.mustache' renderer = Renderer() renderer.file_encoding = 'ascii' @@ -211,7 +211,7 @@ class RendererTestCase(unittest.TestCase): self.assertEquals(actual, u'non-ascii: é') def test_read__decode_errors(self): - filename = 'nonascii.mustache' + filename = 'non_ascii.mustache' renderer = Renderer() self.assertRaises(UnicodeDecodeError, self._read, renderer, filename) diff --git a/tests/test_view.py b/tests/test_view.py index cfd5c46..9c84ae0 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -1,3 +1,10 @@ +# coding: utf-8 + +""" +Unit tests of view.py. + +""" + import os.path import unittest @@ -5,12 +12,13 @@ from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists -from pystache.locator import Locator as TemplateLocator +from pystache.reader import Reader from pystache.view import View from pystache.view import Locator as ViewLocator from .common import AssertIsMixin from .common import DATA_DIR from .data.views import SampleView +from .data.views import NonAscii class Thing(object): @@ -177,19 +185,13 @@ class ViewTestCase(unittest.TestCase): class LocatorTests(unittest.TestCase, AssertIsMixin): def _make_locator(self): - class MockReader(object): - def read(self, path): - return "read: %s" % repr(path) - - reader = MockReader() - template_locator = TemplateLocator() - locator = ViewLocator(reader=reader, search_dirs=[DATA_DIR], template_locator=template_locator) + locator = ViewLocator(search_dirs=[DATA_DIR]) return locator # TODO: fully test constructor. def test_init__reader(self): reader = "reader" # in practice, this is a reader instance. - locator = ViewLocator(reader, search_dirs=None, template_locator=None) + locator = ViewLocator(search_dirs=None, template_locator=None, reader=reader) self.assertIs(locator.reader, reader) @@ -273,26 +275,53 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): self.assertEquals(actual, expected) - def test_get_template__template_attribute_set(self): + def _assert_get_template(self, view, expected): + locator = self._make_locator() + actual = locator.get_template(view) + + self.assertEquals(type(actual), unicode) + self.assertEquals(actual, expected) + + def test_get_template(self): """ - Test get_template() with view.template set to a non-None value. + Test get_template(): default behavior (no attributes set). """ - locator = self._make_locator() - view = View() + view = SampleView() + + self._assert_get_template(view, u"ascii: abc") + + def test_get_template__template(self): + """ + Test get_template(): template attribute. + + """ + view = SampleView() view.template = 'foo' - self.assertEquals(locator.get_template(view), 'foo') + self._assert_get_template(view, 'foo') - def test_get_template__template_attribute_not_set(self): + def test_get_template__template__template_encoding(self): """ - Test get_template() with view.template set to None. + Test get_template(): template attribute with template encoding attribute. """ - locator = self._make_locator() - locator.get_template_path = lambda view: "path" + view = SampleView() + view.template = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') + + view.template_encoding = 'utf-8' + self._assert_get_template(view, u'é') + + def test_get_template__template_encoding(self): + """ + Test get_template(): template_encoding attribute. + + """ + view = NonAscii() - view = View() - view.template = None + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') - self.assertEquals(locator.get_template(view), "read: 'path'") + view.template_encoding = 'utf-8' + self._assert_get_template(view, u"non-ascii: é") -- cgit v1.2.1 From dc732360929e2a58bf4c5c6a745be248887d5f55 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:33:24 -0800 Subject: Added support for view.template_directory. --- pystache/view.py | 9 ++++++++- tests/test_view.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pystache/view.py b/pystache/view.py index 1eceb77..c52997c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -31,6 +31,9 @@ class View(object): template_path: the path to the template file, relative to the directory containing the module defining the class. + template_dir: the directory containing the template file, relative + to the directory containing the module defining the class. + template_extension: the template file extension. Defaults to "mustache". Pass False for no extension (i.e. extensionless template files). @@ -39,6 +42,7 @@ class View(object): template = None template_path = None + template_directory = None template_name = None template_extension = None @@ -157,6 +161,9 @@ class Locator(object): """ if view.template_path is not None: return os.path.split(view.template_path) + + template_dir = view.template_directory + # Otherwise, we don't know the directory. template_name = (view.template_name if view.template_name is not None else @@ -164,7 +171,7 @@ class Locator(object): file_name = self.template_locator.make_file_name(template_name, view.template_extension) - return (None, file_name) + return (template_dir, file_name) def get_template_path(self, view): """ diff --git a/tests/test_view.py b/tests/test_view.py index 9c84ae0..8039169 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -226,6 +226,16 @@ class LocatorTests(unittest.TestCase, AssertIsMixin): view.template_path = 'foo/bar/template.txt' self._assert_template_location(view, ('foo/bar', 'template.txt')) + def test_get_relative_template_location__template_directory(self): + """ + Test get_relative_template_location(): template_directory attribute. + + """ + view = SampleView() + view.template_directory = 'foo' + + self._assert_template_location(view, ('foo', 'sample_view.mustache')) + def test_get_relative_template_location__template_name(self): """ Test get_relative_template_location(): template_name attribute. -- cgit v1.2.1 From d14e40dd77530a8ea1f1bf9806e1fc5ccfdf3f8f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:40:52 -0800 Subject: Removed template from the View constructor. --- pystache/view.py | 5 +---- tests/test_examples.py | 6 ------ tests/test_renderengine.py | 8 ++++++++ tests/test_view.py | 16 ---------------- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index c52997c..60aa386 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -52,7 +52,7 @@ class View(object): locator = TemplateLocator() - def __init__(self, template=None, context=None, partials=None, **kwargs): + def __init__(self, context=None, partials=None, **kwargs): """ Construct a View instance. @@ -66,9 +66,6 @@ class View(object): no template with that name, or raise an exception. """ - if template is not None: - self.template = template - context = Context.create(self, context, **kwargs) self._partials = partials diff --git a/tests/test_examples.py b/tests/test_examples.py index 7be3936..28a4c32 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -37,12 +37,6 @@ class TestView(unittest.TestCase): def test_literal(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") - def test_literal_sigil(self): - view = Escaped(template="

{{& thing}}

", context={ - 'thing': 'Bear > Giraffe' - }) - self.assertEquals(view.render(), "

Bear > Giraffe

") - def test_template_partial(self): self.assertEquals(TemplatePartial().render(), """

Welcome

Again, Welcome!""") diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index e32b3e2..da11ed2 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -92,6 +92,14 @@ class RenderTests(unittest.TestCase): self._assert_render('BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) + def test_literal__sigil(self): + template = "

{{& thing}}

" + context = {'thing': 'Bear > Giraffe'} + + expected = "

Bear > Giraffe

" + + self._assert_render(expected, template, context) + def test__escape(self): """ Test that render() uses the escape attribute. diff --git a/tests/test_view.py b/tests/test_view.py index 8039169..3fca783 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -47,10 +47,6 @@ class ViewTestCase(unittest.TestCase): view = View(context=context, fuzz="buzz") self.assertEquals(context, {"foo": "bar"}) - def test_basic(self): - view = Simple("Hi {{thing}}!", { 'thing': 'world' }) - self.assertEquals(view.render(), "Hi world!") - def test_kwargs(self): view = Simple("Hi {{thing}}!", thing='world') self.assertEquals(view.render(), "Hi world!") @@ -59,18 +55,6 @@ class ViewTestCase(unittest.TestCase): view = Simple(thing='world') self.assertEquals(view.render(), "Hi world!") - def test_render__partials(self): - """ - Test passing partials to View.__init__(). - - """ - template = "{{>partial}}" - partials = {"partial": "Loaded from dictionary"} - view = Simple(template=template, partials=partials) - actual = view.render() - - self.assertEquals(actual, "Loaded from dictionary") - def test_template_path(self): """ Test that View.template_path is respected. -- cgit v1.2.1 From 87deeab9cdba6c440bdc833914f2b9d67211ea0b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:53:30 -0800 Subject: Removed the kwargs argument from the View constructor. --- pystache/view.py | 4 ++-- tests/test_view.py | 37 +------------------------------------ 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 60aa386..1435585 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -52,7 +52,7 @@ class View(object): locator = TemplateLocator() - def __init__(self, context=None, partials=None, **kwargs): + def __init__(self, context=None, partials=None): """ Construct a View instance. @@ -66,7 +66,7 @@ class View(object): no template with that name, or raise an exception. """ - context = Context.create(self, context, **kwargs) + context = Context.create(self, context) self._partials = partials diff --git a/tests/test_view.py b/tests/test_view.py index 3fca783..02f8e53 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -38,23 +38,6 @@ class ViewTestCase(unittest.TestCase): view = TestView() self.assertEquals(view.template, "foo") - def test_init__kwargs_does_not_modify_context(self): - """ - Test that passing **kwargs does not modify the passed context. - - """ - context = {"foo": "bar"} - view = View(context=context, fuzz="buzz") - self.assertEquals(context, {"foo": "bar"}) - - def test_kwargs(self): - view = Simple("Hi {{thing}}!", thing='world') - self.assertEquals(view.render(), "Hi world!") - - def test_render(self): - view = Simple(thing='world') - self.assertEquals(view.render(), "Hi world!") - def test_template_path(self): """ Test that View.template_path is respected. @@ -85,24 +68,6 @@ class ViewTestCase(unittest.TestCase): view.template_path = "examples" self.assertEquals(view.render(), "Partial: No tags...") - def test_template_load_from_multiple_path(self): - path = Simple.template_path - Simple.template_path = ('examples/nowhere','examples',) - try: - view = Simple(thing='world') - self.assertEquals(view.render(), "Hi world!") - finally: - Simple.template_path = path - - def test_template_load_from_multiple_path_fail(self): - path = Simple.template_path - Simple.template_path = ('examples/nowhere',) - try: - view = Simple(thing='world') - self.assertRaises(IOError, view.render) - finally: - Simple.template_path = path - def test_basic_method_calls(self): view = Simple() self.assertEquals(view.render(), "Hi pizza!") @@ -113,7 +78,7 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), "Hi Chris!") def test_view_instances_as_attributes(self): - other = Simple(name='chris') + other = Simple(context={'name': 'chris'}) other.template = '{{name}}' view = Simple() view.thing = other -- cgit v1.2.1 From 18b6c253ab64fc51c162b9a88f5f73367accb68c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:54:47 -0800 Subject: Removed the partials argument from the View constructor. --- pystache/view.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index 1435585..c1dc8cf 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -52,24 +52,13 @@ class View(object): locator = TemplateLocator() - def __init__(self, context=None, partials=None): + def __init__(self, context=None): """ Construct a View instance. - Arguments: - - partials: a custom object (e.g. dictionary) responsible for - loading partials during the rendering process. The object - should have a get() method that accepts a string and returns - the corresponding template as a string, preferably as a - unicode string. The method should return None if there is - no template with that name, or raise an exception. - """ context = Context.create(self, context) - self._partials = partials - self.context = context def _get_renderer(self): @@ -79,8 +68,7 @@ class View(object): # instantiation some of the attributes on which the Renderer # depends. This lets users set the template_extension attribute, # etc. after View.__init__() has already been called. - renderer = Renderer(partials=self._partials, - file_encoding=self.template_encoding, + renderer = Renderer(file_encoding=self.template_encoding, search_dirs=self.template_path, file_extension=self.template_extension) self._renderer = renderer -- cgit v1.2.1 From a28a5fe3b9536c72783cf378168a1d56a545d04d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 18:56:41 -0800 Subject: Removed View.__str__(). --- pystache/view.py | 3 --- tests/test_view.py | 7 ------- 2 files changed, 10 deletions(-) diff --git a/pystache/view.py b/pystache/view.py index c1dc8cf..b38adb7 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -116,9 +116,6 @@ class View(object): def get(self, key, default=None): return self.context.get(key, default) - def __str__(self): - return self.render() - class Locator(object): diff --git a/tests/test_view.py b/tests/test_view.py index 02f8e53..a858d0c 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -77,13 +77,6 @@ class ViewTestCase(unittest.TestCase): view.thing = 'Chris' self.assertEquals(view.render(), "Hi Chris!") - def test_view_instances_as_attributes(self): - other = Simple(context={'name': 'chris'}) - other.template = '{{name}}' - view = Simple() - view.thing = other - self.assertEquals(view.render(), "Hi chris!") - def test_complex(self): self.assertEquals(ComplexView().render(), """

Colors

""") -- cgit v1.2.1 From 638cbc1f2f26fee49e3608648cc549c41eca4efd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 24 Jan 2012 19:06:54 -0800 Subject: Removed View.get(). --- examples/nested_context.py | 6 +++--- examples/template_partial.py | 2 +- pystache/view.py | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/nested_context.py b/examples/nested_context.py index 59d816a..e3f803f 100644 --- a/examples/nested_context.py +++ b/examples/nested_context.py @@ -11,9 +11,9 @@ class NestedContext(pystache.View): def derp(self): return [{'inner': 'car'}] - + def herp(self): return [{'outer': 'car'}] - + def nested_context_in_view(self): - return 'it works!' if self.get('outer') == self.get('inner') else '' \ No newline at end of file + return 'it works!' if self.context.get('outer') == self.context.get('inner') else '' \ No newline at end of file diff --git a/examples/template_partial.py b/examples/template_partial.py index 06ebe38..d3dbfd8 100644 --- a/examples/template_partial.py +++ b/examples/template_partial.py @@ -13,4 +13,4 @@ class TemplatePartial(pystache.View): return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] def thing(self): - return self.get('prop') \ No newline at end of file + return self.context.get('prop') \ No newline at end of file diff --git a/pystache/view.py b/pystache/view.py index b38adb7..0c7f01c 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -113,9 +113,6 @@ class View(object): renderer = self._get_renderer() return renderer.render(template, self.context) - def get(self, key, default=None): - return self.context.get(key, default) - class Locator(object): -- cgit v1.2.1 From dd9184bb05d0b1001b30d61a0202de1273543316 Mon Sep 17 00:00:00 2001 From: Lucas Beyer Date: Wed, 29 Feb 2012 20:50:56 +0100 Subject: Allows to use generators just like lists/tuples. This is done by treating any object with __iter__ methods the way only 'list' and 'tuple' objects were treated before. Except dictionaries, they have an __iter__ method too, but that's not what we want. All 291 tests aswell as the new one pass. --- pystache/renderengine.py | 2 +- tests/test_renderengine.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pystache/renderengine.py b/pystache/renderengine.py index 3fb0ae6..4361dca 100644 --- a/pystache/renderengine.py +++ b/pystache/renderengine.py @@ -168,7 +168,7 @@ class RenderEngine(object): template = data(template) parsed_template = self._parse(template, delimiters=delims) data = [ data ] - elif type(data) not in [list, tuple]: + elif not hasattr(data, '__iter__') or isinstance(data, dict): data = [ data ] parts = [] diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index e32b3e2..5d934a8 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -376,6 +376,22 @@ class RenderTests(unittest.TestCase): context = {'test': (lambda text: 'Hi %s' % text)} self._assert_render('Hi Mom', template, context) + def test_section__generator(self): + """ + Generator expressions should behave the same way as lists do. + This actually tests for everything which has an __iter__ method. + """ + template = '{{#gen}}{{.}}{{/gen}}' + context = {'gen': (i for i in range(5))} + self._assert_render('01234', template, context) + + context = {'gen': xrange(5)} + self._assert_render('01234', template, context) + + d = {'1': 1, '2': 2, 'abc': 3} + context = {'gen': d.iterkeys()} + self._assert_render(''.join(d.keys()), template, context) + def test_section__lambda__tag_in_output(self): """ Check that callable output is treated as a template string (issue #46). -- cgit v1.2.1 From 0b2415f24153dbf62d03f26d5e2cd9d958626fb3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 8 Mar 2012 04:11:57 -0800 Subject: Added documentation on how to push to PyPI. --- setup.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 40c69bb..453faa6 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,31 @@ # coding: utf-8 """ -Run the following to publish to PyPI: +To push a new version of pystache to PyPI-- -> python setup.py publish + http://pypi.python.org/pypi/pystache + +you first need to be added as a "Package Index Owner" of pystache. + +Then run the following (after preparing the release, bumping the version +number, etc): + + > python setup.py publish + +If you get an error like the following-- + + Upload failed (401): You must be identified to edit package information + +then add a file called .pyirc to your home directory with the following +contents: + + [server-login] + username: + password: + +as described here, for example: + + http://docs.python.org/release/2.5.2/dist/pypirc.html """ -- cgit v1.2.1 From afc0e7859f75d34a0e28329111056966afb904bd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 9 Mar 2012 05:42:43 -0800 Subject: Tweaks to the setup.py docstring. --- setup.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 453faa6..da8f8d7 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,19 @@ # coding: utf-8 """ -To push a new version of pystache to PyPI-- +This script supports installing and distributing pystache. + +Below are instructions to pystache maintainers on how to push a new +version of pystache to PyPI-- http://pypi.python.org/pypi/pystache -you first need to be added as a "Package Index Owner" of pystache. +Create a PyPI user account. The user account will need permissions to push +to PyPI. A current "Package Index Owner" of pystache can grant you those +permissions. -Then run the following (after preparing the release, bumping the version -number, etc): +When you have permissions, run the following (after preparing the release, +bumping the version number in setup.py, etc): > python setup.py publish -- cgit v1.2.1 From 049c0d83519d976028371b80ee3537120d2688ed Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 9 Mar 2012 05:45:28 -0800 Subject: Added 2012 to copyright. --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 2745bcc..1943585 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ +Copyright (C) 2012 Chris Jerdonek. All rights reserved. Copyright (c) 2009 Chris Wanstrath Permission is hereby granted, free of charge, to any person obtaining -- cgit v1.2.1 From f7df3412067994a7984e67c303de66c6c9ff81aa Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 10 Mar 2012 12:09:18 -0800 Subject: Fixed ImportErrors when running nosetests (couldn't import tests.common). --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.1 From 864957a52d166e79a72b39a9d490d6c134d59eb9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Mar 2012 16:37:40 -0700 Subject: Added a setup.cfg file (to store nosetests options). --- README.rst | 4 ++++ setup.cfg | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 setup.cfg diff --git a/README.rst b/README.rst index bbacd7a..2f4d7c5 100644 --- a/README.rst +++ b/README.rst @@ -84,6 +84,10 @@ To run all available tests (including doctests):: nosetests --with-doctest --doctest-extension=rst +or alternatively:: + + python setup.py nosetests + Mailing List ================== diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f91c44e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[nosetests] +with-doctest=1 +doctest-extension=rst -- cgit v1.2.1 From 9148826d0ca06796ff611bb6f1f985c1f32906bc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Mar 2012 16:45:28 -0700 Subject: Adjusted intro paragraphs of README. --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2f4d7c5..fb7e1e5 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,8 @@ Pystache .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png +Pystache_ is a Python implementation of Mustache. + Mustache_ is a framework-agnostic way to render logic-free views that is inspired by ctemplate_ and et_. Like ctemplate_, "it emphasizes separating logic from presentation: it is impossible to embed application @@ -13,9 +15,8 @@ The `mustache(5)`_ man page provides a good introduction to Mustache's syntax. For a more complete (and more current) description of Mustache's behavior, see the official Mustache spec_. -Pystache_ is a Python implementation of Mustache. It currently passes -all tests in `version 1.0.3`_ of the Mustache spec_. Pystache itself is -`semantically versioned`_. +Pystache is `semantically versioned`_. This version of Pystache passes all +tests in `version 1.0.3`_ of the Mustache spec_. Logo: `David Phillips`_ -- cgit v1.2.1 From 9d082532e299e869a1ab3d879e2dac7a81cb3bce Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Mar 2012 19:14:30 -0700 Subject: Added CustomizedTemplate class. --- examples/comments.mustache | 2 +- examples/comments.py | 4 ++-- pystache/init.py | 3 ++- pystache/view.py | 7 +++++-- tests/test_examples.py | 9 +++++---- tests/test_renderer.py | 1 - 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/examples/comments.mustache b/examples/comments.mustache index 8bdfb5e..2a2a08b 100644 --- a/examples/comments.mustache +++ b/examples/comments.mustache @@ -1 +1 @@ -

{{title}}{{! just something interesting... #or not... }}

+

{{title}}{{! just something interesting... #or not... }}

\ No newline at end of file diff --git a/examples/comments.py b/examples/comments.py index 1d2ed0b..c1c246e 100644 --- a/examples/comments.py +++ b/examples/comments.py @@ -1,6 +1,6 @@ -import pystache +from pystache import CustomizedTemplate -class Comments(pystache.View): +class Comments(CustomizedTemplate): template_path = 'examples' def title(self): diff --git a/pystache/init.py b/pystache/init.py index 1a60028..3054129 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -7,9 +7,10 @@ This module contains the initialization logic called by __init__.py. from .renderer import Renderer from .view import View +from .view import CustomizedTemplate -__all__ = ['render', 'Renderer', 'View'] +__all__ = ['render', 'Renderer', 'View', 'CustomizedTemplate'] def render(template, context=None, **kwargs): diff --git a/pystache/view.py b/pystache/view.py index 0c7f01c..9befa93 100644 --- a/pystache/view.py +++ b/pystache/view.py @@ -13,8 +13,7 @@ from .reader import Reader from .renderer import Renderer -# TODO: rename this class to something else (e.g. ITemplateInfo) -class View(object): +class CustomizedTemplate(object): """ Subclass this class only if template customizations are needed. @@ -48,6 +47,10 @@ class View(object): template_encoding = None + +# TODO: remove this class. +class View(CustomizedTemplate): + _renderer = None locator = TemplateLocator() diff --git a/tests/test_examples.py b/tests/test_examples.py index 28a4c32..fd044cb 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,7 +1,6 @@ # encoding: utf-8 import unittest -import pystache from examples.comments import Comments from examples.double_section import DoubleSection @@ -12,14 +11,16 @@ from examples.delimiters import Delimiters from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext - +from pystache.renderer import Renderer from tests.common import assert_strings class TestView(unittest.TestCase): + def test_comments(self): - self.assertEquals(Comments().render(), """

A Comedy of Errors

-""") + renderer = Renderer() + expected = renderer.render(Comments()) + self.assertEquals(expected, "

A Comedy of Errors

") def test_double_section(self): self.assertEquals(DoubleSection().render(),"""* first\n* second\n* third""") diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 3db57eb..7a9b7cf 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -11,7 +11,6 @@ import sys import unittest from examples.simple import Simple -from pystache import renderer from pystache.renderer import Renderer from pystache.locator import Locator -- cgit v1.2.1 From a606435511eec24221eace0d747baf0974ced115 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 18 Mar 2012 19:16:13 -0700 Subject: Comments example no longer inherits from CustomizedTemplate. --- examples/comments.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/comments.py b/examples/comments.py index c1c246e..f9c3125 100644 --- a/examples/comments.py +++ b/examples/comments.py @@ -1,7 +1,4 @@ -from pystache import CustomizedTemplate - -class Comments(CustomizedTemplate): - template_path = 'examples' +class Comments(object): def title(self): return "A Comedy of Errors" -- cgit v1.2.1 From 743ed3c2242c2a28bfa54f647580b5d62549517a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Mar 2012 17:45:02 -0700 Subject: The "ComplexView" tests no longer depend on pystache.View. --- examples/complex_view.mustache | 7 ++++++- examples/complex_view.py | 6 ++---- tests/test_examples.py | 2 +- tests/test_simple.py | 10 ++++++---- tests/test_view.py | 12 ++++++++++-- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/examples/complex_view.mustache b/examples/complex_view.mustache index 45ea706..6de758b 100644 --- a/examples/complex_view.mustache +++ b/examples/complex_view.mustache @@ -1 +1,6 @@ -

{{ header }}

{{#list}}
    {{#item}}{{# current }}
  • {{name}}
  • {{/ current }}{{#link}}
  • {{name}}
  • {{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}} \ No newline at end of file +

{{ header }}

+{{#list}} +
    +{{#item}}{{# current }}
  • {{name}}
  • +{{/ current }}{{#link}}
  • {{name}}
  • +{{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}} \ No newline at end of file diff --git a/examples/complex_view.py b/examples/complex_view.py index ef45ff7..8dba0a2 100644 --- a/examples/complex_view.py +++ b/examples/complex_view.py @@ -1,6 +1,4 @@ -import pystache - -class ComplexView(pystache.View): +class ComplexView(object): template_path = 'examples' def header(self): @@ -18,6 +16,6 @@ class ComplexView(pystache.View): def empty(self): return len(self.item()) == 0 - + def empty_list(self): return []; diff --git a/tests/test_examples.py b/tests/test_examples.py index fd044cb..e7e3460 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -11,7 +11,7 @@ from examples.delimiters import Delimiters from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext -from pystache.renderer import Renderer +from pystache import Renderer from tests.common import assert_strings diff --git a/tests/test_simple.py b/tests/test_simple.py index da85324..91661f9 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -19,12 +19,14 @@ class TestSimple(unittest.TestCase): self.assertEquals(view.render(), "one and foo and two") def test_looping_and_negation_context(self): - view = ComplexView() - view.template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' - self.assertEquals(view.render(), "Colors: red Colors: green Colors: blue ") + template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' + context = ComplexView() + + renderer = Renderer() + expected = renderer.render(template, context) + self.assertEquals(expected, "Colors: red Colors: green Colors: blue ") def test_empty_context(self): - view = ComplexView() template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' self.assertEquals(pystache.Renderer().render(template), "Should see me") diff --git a/tests/test_view.py b/tests/test_view.py index a858d0c..3888b24 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -12,6 +12,7 @@ from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists +from pystache import Renderer from pystache.reader import Reader from pystache.view import View from pystache.view import Locator as ViewLocator @@ -78,8 +79,15 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), "Hi Chris!") def test_complex(self): - self.assertEquals(ComplexView().render(), - """

Colors

""") + renderer = Renderer() + expected = renderer.render(ComplexView()) + self.assertEquals(expected, """\ +

Colors

+""") def test_higher_order_replace(self): view = Lambdas() -- cgit v1.2.1 From d3fa1867546f9d4a61d471d24d3a0135455af36c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Mar 2012 17:47:39 -0700 Subject: Removed an unnecessary property from the ComplexView class. --- examples/complex_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/complex_view.py b/examples/complex_view.py index 8dba0a2..6edbbb5 100644 --- a/examples/complex_view.py +++ b/examples/complex_view.py @@ -1,5 +1,4 @@ class ComplexView(object): - template_path = 'examples' def header(self): return "Colors" -- cgit v1.2.1 From 1055dd9b76e6fc4254edf3a8befbe95118435f68 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Mar 2012 17:48:49 -0700 Subject: Merged first two paragraphs of README. --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index fb7e1e5..c6562da 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,6 @@ Pystache .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png Pystache_ is a Python implementation of Mustache. - Mustache_ is a framework-agnostic way to render logic-free views that is inspired by ctemplate_ and et_. Like ctemplate_, "it emphasizes separating logic from presentation: it is impossible to embed application -- cgit v1.2.1 From 17d70ba678b3413a5934e09a8321885675f6238b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Mar 2012 17:51:44 -0700 Subject: Delimiters test no longer depends on pystache.View. --- examples/delimiters.py | 5 +---- tests/test_examples.py | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/delimiters.py b/examples/delimiters.py index 53f65f2..a132ed0 100644 --- a/examples/delimiters.py +++ b/examples/delimiters.py @@ -1,7 +1,4 @@ -import pystache - -class Delimiters(pystache.View): - template_path = 'examples' +class Delimiters(object): def first(self): return "It worked the first time." diff --git a/tests/test_examples.py b/tests/test_examples.py index e7e3460..4493179 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -51,7 +51,10 @@ Again, Welcome!""") ## Again, Welcome! ##""") def test_delimiters(self): - assert_strings(self, Delimiters().render(), """* It worked the first time. + renderer = Renderer() + expected = renderer.render(Delimiters()) + assert_strings(self, expected, """\ +* It worked the first time. * And it worked the second time. * Then, surprisingly, it worked the third time. """) -- cgit v1.2.1 From 370d52544f3e316a041892fbf33710ce235e4915 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 19 Mar 2012 18:00:16 -0700 Subject: DoubleSection and Escaped tests no longer depend on pystache.View. --- examples/double_section.py | 5 +---- examples/escaped.py | 5 +---- tests/test_examples.py | 17 ++++++++++------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/double_section.py b/examples/double_section.py index 7085739..0bec602 100644 --- a/examples/double_section.py +++ b/examples/double_section.py @@ -1,7 +1,4 @@ -import pystache - -class DoubleSection(pystache.View): - template_path = 'examples' +class DoubleSection(object): def t(self): return True diff --git a/examples/escaped.py b/examples/escaped.py index ddaad4f..fed1705 100644 --- a/examples/escaped.py +++ b/examples/escaped.py @@ -1,7 +1,4 @@ -import pystache - -class Escaped(pystache.View): - template_path = 'examples' +class Escaped(object): def title(self): return "Bear > Shark" diff --git a/tests/test_examples.py b/tests/test_examples.py index 4493179..5c95de9 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -17,13 +17,16 @@ from tests.common import assert_strings class TestView(unittest.TestCase): - def test_comments(self): + def _assert(self, obj, expected): renderer = Renderer() - expected = renderer.render(Comments()) - self.assertEquals(expected, "

A Comedy of Errors

") + actual = renderer.render(obj) + assert_strings(self, actual, expected) + + def test_comments(self): + self._assert(Comments(), "

A Comedy of Errors

") def test_double_section(self): - self.assertEquals(DoubleSection().render(),"""* first\n* second\n* third""") + self._assert(DoubleSection(), "* first\n* second\n* third") def test_unicode_output(self): self.assertEquals(UnicodeOutput().render(), u'

Name: Henri Poincaré

') @@ -33,7 +36,7 @@ class TestView(unittest.TestCase): u'

If alive today, Henri Poincaré would be 156 years old.

') def test_escaping(self): - self.assertEquals(Escaped().render(), "

Bear > Shark

") + self._assert(Escaped(), "

Bear > Shark

") def test_literal(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") @@ -52,8 +55,8 @@ Again, Welcome!""") def test_delimiters(self): renderer = Renderer() - expected = renderer.render(Delimiters()) - assert_strings(self, expected, """\ + actual = renderer.render(Delimiters()) + assert_strings(self, actual, """\ * It worked the first time. * And it worked the second time. * Then, surprisingly, it worked the third time. -- cgit v1.2.1 From 17b144e21df1a6f0f1df5a48289356e94698c834 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 01:18:41 -0700 Subject: Tweaked introduction to README. --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index c6562da..e184107 100644 --- a/README.rst +++ b/README.rst @@ -4,18 +4,18 @@ Pystache .. image:: https://s3.amazonaws.com/webdev_bucket/pystache.png -Pystache_ is a Python implementation of Mustache. -Mustache_ is a framework-agnostic way to render logic-free views that is -inspired by ctemplate_ and et_. Like ctemplate_, "it emphasizes +Pystache_ is a Python implementation of Mustache_. +Mustache is a framework-agnostic, logic-free templating system inspired +by ctemplate_ and et_. Like ctemplate, Mustache "emphasizes separating logic from presentation: it is impossible to embed application logic in this template language." The `mustache(5)`_ man page provides a good introduction to Mustache's syntax. For a more complete (and more current) description of Mustache's -behavior, see the official Mustache spec_. +behavior, see the official `Mustache spec`_. Pystache is `semantically versioned`_. This version of Pystache passes all -tests in `version 1.0.3`_ of the Mustache spec_. +tests in `version 1.0.3`_ of the spec. Logo: `David Phillips`_ @@ -75,7 +75,7 @@ nose_ works great! :: cd pystache nosetests -To include tests from the Mustache spec_ in your test runs: :: +To include tests from the Mustache spec in your test runs: :: git submodule init git submodule update @@ -113,9 +113,9 @@ Author .. _David Phillips: http://davidphillips.us/ .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html .. _Mustache: http://mustache.github.com/ +.. _Mustache spec: https://github.com/mustache/spec .. _mustache(5): http://mustache.github.com/mustache.5.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org -.. _spec: https://github.com/mustache/spec .. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From 87987fe55edda34b882912a2662ef36c730b8a98 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 01:37:51 -0700 Subject: Added to README mention of setup.cfg. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e184107..1753046 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,7 @@ To run all available tests (including doctests):: nosetests --with-doctest --doctest-extension=rst -or alternatively:: +or alternatively (using setup.cfg):: python setup.py nosetests -- cgit v1.2.1 From 4edb9fa0cdba7d5f43167086dcba7f8141e9aad6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 04:07:45 -0700 Subject: Renamed view module to custom_template. --- pystache/custom_template.py | 184 ++++++++++++++++++++++++++++++++++++++++++++ pystache/init.py | 4 +- pystache/view.py | 184 -------------------------------------------- tests/data/views.py | 2 +- tests/test_view.py | 4 +- 5 files changed, 189 insertions(+), 189 deletions(-) create mode 100644 pystache/custom_template.py delete mode 100644 pystache/view.py diff --git a/pystache/custom_template.py b/pystache/custom_template.py new file mode 100644 index 0000000..9befa93 --- /dev/null +++ b/pystache/custom_template.py @@ -0,0 +1,184 @@ +# coding: utf-8 + +""" +This module provides a View class. + +""" + +import os.path + +from .context import Context +from .locator import Locator as TemplateLocator +from .reader import Reader +from .renderer import Renderer + + +class CustomizedTemplate(object): + + """ + Subclass this class only if template customizations are needed. + + The following attributes allow one to customize/override template + information on a per View basis. A None value means to use default + behavior and perform no customization. All attributes are initially + set to None. + + Attributes: + + template: the template to use, as a unicode string. + + template_path: the path to the template file, relative to the + directory containing the module defining the class. + + template_dir: the directory containing the template file, relative + to the directory containing the module defining the class. + + template_extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + + """ + + template = None + template_path = None + + template_directory = None + template_name = None + template_extension = None + + template_encoding = None + + +# TODO: remove this class. +class View(CustomizedTemplate): + + _renderer = None + + locator = TemplateLocator() + + def __init__(self, context=None): + """ + Construct a View instance. + + """ + context = Context.create(self, context) + + self.context = context + + def _get_renderer(self): + if self._renderer is None: + # We delay setting self._renderer until now (instead of, say, + # setting it in the constructor) in case the user changes after + # instantiation some of the attributes on which the Renderer + # depends. This lets users set the template_extension attribute, + # etc. after View.__init__() has already been called. + renderer = Renderer(file_encoding=self.template_encoding, + search_dirs=self.template_path, + file_extension=self.template_extension) + self._renderer = renderer + + return self._renderer + + def get_template(self): + """ + Return the current template after setting it, if necessary. + + """ + if not self.template: + template_name = self._get_template_name() + renderer = self._get_renderer() + self.template = renderer.load_template(template_name) + + return self.template + + def _get_template_name(self): + """ + Return the name of the template to load. + + If the template_name attribute is not set, then this method constructs + the template name from the class name as follows, for example: + + TemplatePartial => template_partial + + Otherwise, this method returns the template_name. + + """ + if self.template_name: + return self.template_name + + return self.locator.make_template_name(self) + + def render(self): + """ + Return the view rendered using the current context. + + """ + template = self.get_template() + renderer = self._get_renderer() + return renderer.render(template, self.context) + + +class Locator(object): + + """ + A class for finding the template associated to a View instance. + + """ + + # TODO: unit test this. + def __init__(self, search_dirs, template_locator=None, reader=None): + if reader is None: + reader = Reader() + + if template_locator is None: + template_locator = TemplateLocator() + + self.reader = reader + self.search_dirs = search_dirs + self.template_locator = template_locator + + def get_relative_template_location(self, view): + """ + Return the relative template path as a (dir, file_name) pair. + + """ + if view.template_path is not None: + return os.path.split(view.template_path) + + template_dir = view.template_directory + + # Otherwise, we don't know the directory. + + template_name = (view.template_name if view.template_name is not None else + self.template_locator.make_template_name(view)) + + file_name = self.template_locator.make_file_name(template_name, view.template_extension) + + return (template_dir, file_name) + + def get_template_path(self, view): + """ + Return the path to the view's associated template. + + """ + dir_path, file_name = self.get_relative_template_location(view) + + if dir_path is None: + # Then we need to search for the path. + path = self.template_locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + else: + obj_dir = self.template_locator.get_object_directory(view) + path = os.path.join(obj_dir, dir_path, file_name) + + return path + + def get_template(self, view): + """ + Return the unicode template string associated with a view. + + """ + if view.template is not None: + return self.reader.unicode(view.template, view.template_encoding) + + path = self.get_template_path(view) + + return self.reader.read(path, view.template_encoding) diff --git a/pystache/init.py b/pystache/init.py index 3054129..75c34a5 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,9 +5,9 @@ This module contains the initialization logic called by __init__.py. """ +from .custom_template import View +from .custom_template import CustomizedTemplate from .renderer import Renderer -from .view import View -from .view import CustomizedTemplate __all__ = ['render', 'Renderer', 'View', 'CustomizedTemplate'] diff --git a/pystache/view.py b/pystache/view.py deleted file mode 100644 index 9befa93..0000000 --- a/pystache/view.py +++ /dev/null @@ -1,184 +0,0 @@ -# coding: utf-8 - -""" -This module provides a View class. - -""" - -import os.path - -from .context import Context -from .locator import Locator as TemplateLocator -from .reader import Reader -from .renderer import Renderer - - -class CustomizedTemplate(object): - - """ - Subclass this class only if template customizations are needed. - - The following attributes allow one to customize/override template - information on a per View basis. A None value means to use default - behavior and perform no customization. All attributes are initially - set to None. - - Attributes: - - template: the template to use, as a unicode string. - - template_path: the path to the template file, relative to the - directory containing the module defining the class. - - template_dir: the directory containing the template file, relative - to the directory containing the module defining the class. - - template_extension: the template file extension. Defaults to "mustache". - Pass False for no extension (i.e. extensionless template files). - - """ - - template = None - template_path = None - - template_directory = None - template_name = None - template_extension = None - - template_encoding = None - - -# TODO: remove this class. -class View(CustomizedTemplate): - - _renderer = None - - locator = TemplateLocator() - - def __init__(self, context=None): - """ - Construct a View instance. - - """ - context = Context.create(self, context) - - self.context = context - - def _get_renderer(self): - if self._renderer is None: - # We delay setting self._renderer until now (instead of, say, - # setting it in the constructor) in case the user changes after - # instantiation some of the attributes on which the Renderer - # depends. This lets users set the template_extension attribute, - # etc. after View.__init__() has already been called. - renderer = Renderer(file_encoding=self.template_encoding, - search_dirs=self.template_path, - file_extension=self.template_extension) - self._renderer = renderer - - return self._renderer - - def get_template(self): - """ - Return the current template after setting it, if necessary. - - """ - if not self.template: - template_name = self._get_template_name() - renderer = self._get_renderer() - self.template = renderer.load_template(template_name) - - return self.template - - def _get_template_name(self): - """ - Return the name of the template to load. - - If the template_name attribute is not set, then this method constructs - the template name from the class name as follows, for example: - - TemplatePartial => template_partial - - Otherwise, this method returns the template_name. - - """ - if self.template_name: - return self.template_name - - return self.locator.make_template_name(self) - - def render(self): - """ - Return the view rendered using the current context. - - """ - template = self.get_template() - renderer = self._get_renderer() - return renderer.render(template, self.context) - - -class Locator(object): - - """ - A class for finding the template associated to a View instance. - - """ - - # TODO: unit test this. - def __init__(self, search_dirs, template_locator=None, reader=None): - if reader is None: - reader = Reader() - - if template_locator is None: - template_locator = TemplateLocator() - - self.reader = reader - self.search_dirs = search_dirs - self.template_locator = template_locator - - def get_relative_template_location(self, view): - """ - Return the relative template path as a (dir, file_name) pair. - - """ - if view.template_path is not None: - return os.path.split(view.template_path) - - template_dir = view.template_directory - - # Otherwise, we don't know the directory. - - template_name = (view.template_name if view.template_name is not None else - self.template_locator.make_template_name(view)) - - file_name = self.template_locator.make_file_name(template_name, view.template_extension) - - return (template_dir, file_name) - - def get_template_path(self, view): - """ - Return the path to the view's associated template. - - """ - dir_path, file_name = self.get_relative_template_location(view) - - if dir_path is None: - # Then we need to search for the path. - path = self.template_locator.find_path_by_object(self.search_dirs, view, file_name=file_name) - else: - obj_dir = self.template_locator.get_object_directory(view) - path = os.path.join(obj_dir, dir_path, file_name) - - return path - - def get_template(self, view): - """ - Return the unicode template string associated with a view. - - """ - if view.template is not None: - return self.reader.unicode(view.template, view.template_encoding) - - path = self.get_template_path(view) - - return self.reader.read(path, view.template_encoding) diff --git a/tests/data/views.py b/tests/data/views.py index c082abe..57b5a7d 100644 --- a/tests/data/views.py +++ b/tests/data/views.py @@ -1,6 +1,6 @@ # coding: utf-8 -from pystache.view import View +from pystache import View class SayHello(object): diff --git a/tests/test_view.py b/tests/test_view.py index 3888b24..e5c8144 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -13,9 +13,9 @@ from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache import Renderer +from pystache import View from pystache.reader import Reader -from pystache.view import View -from pystache.view import Locator as ViewLocator +from pystache.custom_template import Locator as ViewLocator from .common import AssertIsMixin from .common import DATA_DIR from .data.views import SampleView -- cgit v1.2.1 From 8c2e922c0dd7ed1ca949399b672916a012e2eff0 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 04:12:50 -0700 Subject: Updated custom_template module docstring. --- pystache/custom_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 9befa93..4fd9223 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module provides a View class. +This module supports specifying custom template information per view. """ -- cgit v1.2.1 From afb12bbe2203cd2de3ace5b218b27a81e4ebd40e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 04:25:55 -0700 Subject: Renamed custom_template.Locator to custom_template.Loader. --- pystache/custom_template.py | 6 ++++-- tests/test_view.py | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 4fd9223..40f63b9 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -16,6 +16,8 @@ from .renderer import Renderer class CustomizedTemplate(object): """ + A mixin for specifying custom template information. + Subclass this class only if template customizations are needed. The following attributes allow one to customize/override template @@ -117,10 +119,10 @@ class View(CustomizedTemplate): return renderer.render(template, self.context) -class Locator(object): +class Loader(object): """ - A class for finding the template associated to a View instance. + Supports loading the template of a CustomizedTemplate instance. """ diff --git a/tests/test_view.py b/tests/test_view.py index e5c8144..6f21058 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -15,7 +15,7 @@ from examples.inverted import Inverted, InvertedLists from pystache import Renderer from pystache import View from pystache.reader import Reader -from pystache.custom_template import Locator as ViewLocator +from pystache.custom_template import Loader from .common import AssertIsMixin from .common import DATA_DIR from .data.views import SampleView @@ -132,16 +132,17 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -class LocatorTests(unittest.TestCase, AssertIsMixin): +class LoaderTests(unittest.TestCase, AssertIsMixin): + # TODO: rename this method to _make_loader(). def _make_locator(self): - locator = ViewLocator(search_dirs=[DATA_DIR]) + locator = Loader(search_dirs=[DATA_DIR]) return locator # TODO: fully test constructor. def test_init__reader(self): reader = "reader" # in practice, this is a reader instance. - locator = ViewLocator(search_dirs=None, template_locator=None, reader=reader) + locator = Loader(search_dirs=None, template_locator=None, reader=reader) self.assertIs(locator.reader, reader) -- cgit v1.2.1 From 8431c10e0d459d905d246de8f7e3e369b2dda663 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 04:27:03 -0700 Subject: Renamed tests/test_view.py to tests/test_custom_template.py. --- tests/test_custom_template.py | 288 ++++++++++++++++++++++++++++++++++++++++++ tests/test_view.py | 288 ------------------------------------------ 2 files changed, 288 insertions(+), 288 deletions(-) create mode 100644 tests/test_custom_template.py delete mode 100644 tests/test_view.py diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py new file mode 100644 index 0000000..6f21058 --- /dev/null +++ b/tests/test_custom_template.py @@ -0,0 +1,288 @@ +# coding: utf-8 + +""" +Unit tests of view.py. + +""" + +import os.path +import unittest + +from examples.simple import Simple +from examples.complex_view import ComplexView +from examples.lambdas import Lambdas +from examples.inverted import Inverted, InvertedLists +from pystache import Renderer +from pystache import View +from pystache.reader import Reader +from pystache.custom_template import Loader +from .common import AssertIsMixin +from .common import DATA_DIR +from .data.views import SampleView +from .data.views import NonAscii + + +class Thing(object): + pass + + +class ViewTestCase(unittest.TestCase): + + def test_init(self): + """ + Test the constructor. + + """ + class TestView(View): + template = "foo" + + view = TestView() + self.assertEquals(view.template, "foo") + + def test_template_path(self): + """ + Test that View.template_path is respected. + + """ + class Tagless(View): + pass + + view = Tagless() + self.assertRaises(IOError, view.render) + + view = Tagless() + view.template_path = "examples" + self.assertEquals(view.render(), "No tags...") + + def test_template_path_for_partials(self): + """ + Test that View.template_path is respected for partials. + + """ + class TestView(View): + template = "Partial: {{>tagless}}" + + view = TestView() + self.assertRaises(IOError, view.render) + + view = TestView() + view.template_path = "examples" + self.assertEquals(view.render(), "Partial: No tags...") + + def test_basic_method_calls(self): + view = Simple() + self.assertEquals(view.render(), "Hi pizza!") + + def test_non_callable_attributes(self): + view = Simple() + view.thing = 'Chris' + self.assertEquals(view.render(), "Hi Chris!") + + def test_complex(self): + renderer = Renderer() + expected = renderer.render(ComplexView()) + self.assertEquals(expected, """\ +

Colors

+""") + + def test_higher_order_replace(self): + view = Lambdas() + self.assertEquals(view.render(), + 'bar != bar. oh, it does!') + + def test_higher_order_rot13(self): + view = Lambdas() + view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' + self.assertEquals(view.render(), 'nopqrstuvwxyz') + + def test_higher_order_lambda(self): + view = Lambdas() + view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' + self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') + + def test_partials_with_lambda(self): + view = Lambdas() + view.template = '{{>partial_with_lambda}}' + self.assertEquals(view.render(), 'nopqrstuvwxyz') + + def test_hierarchical_partials_with_lambdas(self): + view = Lambdas() + view.template = '{{>partial_with_partial_and_lambda}}' + self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') + + def test_inverted(self): + view = Inverted() + self.assertEquals(view.render(), """one, two, three, empty list""") + + def test_accessing_properties_on_parent_object_from_child_objects(self): + parent = Thing() + parent.this = 'derp' + parent.children = [Thing()] + view = Simple(context={'parent': parent}) + view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" + + self.assertEquals(view.render(), 'derp') + + def test_inverted_lists(self): + view = InvertedLists() + self.assertEquals(view.render(), """one, two, three, empty list""") + + +class LoaderTests(unittest.TestCase, AssertIsMixin): + + # TODO: rename this method to _make_loader(). + def _make_locator(self): + locator = Loader(search_dirs=[DATA_DIR]) + return locator + + # TODO: fully test constructor. + def test_init__reader(self): + reader = "reader" # in practice, this is a reader instance. + locator = Loader(search_dirs=None, template_locator=None, reader=reader) + + self.assertIs(locator.reader, reader) + + def _assert_template_location(self, view, expected): + locator = self._make_locator() + actual = locator.get_relative_template_location(view) + self.assertEquals(actual, expected) + + def test_get_relative_template_location(self): + """ + Test get_relative_template_location(): default behavior (no attributes set). + + """ + view = SampleView() + self._assert_template_location(view, (None, 'sample_view.mustache')) + + def test_get_relative_template_location__template_path__file_name_only(self): + """ + Test get_relative_template_location(): template_path attribute. + + """ + view = SampleView() + view.template_path = 'template.txt' + self._assert_template_location(view, ('', 'template.txt')) + + def test_get_relative_template_location__template_path__file_name_with_directory(self): + """ + Test get_relative_template_location(): template_path attribute. + + """ + view = SampleView() + view.template_path = 'foo/bar/template.txt' + self._assert_template_location(view, ('foo/bar', 'template.txt')) + + def test_get_relative_template_location__template_directory(self): + """ + Test get_relative_template_location(): template_directory attribute. + + """ + view = SampleView() + view.template_directory = 'foo' + + self._assert_template_location(view, ('foo', 'sample_view.mustache')) + + def test_get_relative_template_location__template_name(self): + """ + Test get_relative_template_location(): template_name attribute. + + """ + view = SampleView() + view.template_name = 'new_name' + self._assert_template_location(view, (None, 'new_name.mustache')) + + def test_get_relative_template_location__template_extension(self): + """ + Test get_relative_template_location(): template_extension attribute. + + """ + view = SampleView() + view.template_extension = 'txt' + self._assert_template_location(view, (None, 'sample_view.txt')) + + def test_get_template_path__with_directory(self): + """ + Test get_template_path() with a view that has a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + view.template_path = 'foo/bar.txt' + self.assertTrue(locator.get_relative_template_location(view)[0] is not None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) + + self.assertEquals(actual, expected) + + def test_get_template_path__without_directory(self): + """ + Test get_template_path() with a view that doesn't have a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + self.assertTrue(locator.get_relative_template_location(view)[0] is None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) + + self.assertEquals(actual, expected) + + def _assert_get_template(self, view, expected): + locator = self._make_locator() + actual = locator.get_template(view) + + self.assertEquals(type(actual), unicode) + self.assertEquals(actual, expected) + + def test_get_template(self): + """ + Test get_template(): default behavior (no attributes set). + + """ + view = SampleView() + + self._assert_get_template(view, u"ascii: abc") + + def test_get_template__template(self): + """ + Test get_template(): template attribute. + + """ + view = SampleView() + view.template = 'foo' + + self._assert_get_template(view, 'foo') + + def test_get_template__template__template_encoding(self): + """ + Test get_template(): template attribute with template encoding attribute. + + """ + view = SampleView() + view.template = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') + + view.template_encoding = 'utf-8' + self._assert_get_template(view, u'é') + + def test_get_template__template_encoding(self): + """ + Test get_template(): template_encoding attribute. + + """ + view = NonAscii() + + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') + + view.template_encoding = 'utf-8' + self._assert_get_template(view, u"non-ascii: é") diff --git a/tests/test_view.py b/tests/test_view.py deleted file mode 100644 index 6f21058..0000000 --- a/tests/test_view.py +++ /dev/null @@ -1,288 +0,0 @@ -# coding: utf-8 - -""" -Unit tests of view.py. - -""" - -import os.path -import unittest - -from examples.simple import Simple -from examples.complex_view import ComplexView -from examples.lambdas import Lambdas -from examples.inverted import Inverted, InvertedLists -from pystache import Renderer -from pystache import View -from pystache.reader import Reader -from pystache.custom_template import Loader -from .common import AssertIsMixin -from .common import DATA_DIR -from .data.views import SampleView -from .data.views import NonAscii - - -class Thing(object): - pass - - -class ViewTestCase(unittest.TestCase): - - def test_init(self): - """ - Test the constructor. - - """ - class TestView(View): - template = "foo" - - view = TestView() - self.assertEquals(view.template, "foo") - - def test_template_path(self): - """ - Test that View.template_path is respected. - - """ - class Tagless(View): - pass - - view = Tagless() - self.assertRaises(IOError, view.render) - - view = Tagless() - view.template_path = "examples" - self.assertEquals(view.render(), "No tags...") - - def test_template_path_for_partials(self): - """ - Test that View.template_path is respected for partials. - - """ - class TestView(View): - template = "Partial: {{>tagless}}" - - view = TestView() - self.assertRaises(IOError, view.render) - - view = TestView() - view.template_path = "examples" - self.assertEquals(view.render(), "Partial: No tags...") - - def test_basic_method_calls(self): - view = Simple() - self.assertEquals(view.render(), "Hi pizza!") - - def test_non_callable_attributes(self): - view = Simple() - view.thing = 'Chris' - self.assertEquals(view.render(), "Hi Chris!") - - def test_complex(self): - renderer = Renderer() - expected = renderer.render(ComplexView()) - self.assertEquals(expected, """\ -

Colors

-""") - - def test_higher_order_replace(self): - view = Lambdas() - self.assertEquals(view.render(), - 'bar != bar. oh, it does!') - - def test_higher_order_rot13(self): - view = Lambdas() - view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') - - def test_higher_order_lambda(self): - view = Lambdas() - view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' - self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') - - def test_partials_with_lambda(self): - view = Lambdas() - view.template = '{{>partial_with_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') - - def test_hierarchical_partials_with_lambdas(self): - view = Lambdas() - view.template = '{{>partial_with_partial_and_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') - - def test_inverted(self): - view = Inverted() - self.assertEquals(view.render(), """one, two, three, empty list""") - - def test_accessing_properties_on_parent_object_from_child_objects(self): - parent = Thing() - parent.this = 'derp' - parent.children = [Thing()] - view = Simple(context={'parent': parent}) - view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" - - self.assertEquals(view.render(), 'derp') - - def test_inverted_lists(self): - view = InvertedLists() - self.assertEquals(view.render(), """one, two, three, empty list""") - - -class LoaderTests(unittest.TestCase, AssertIsMixin): - - # TODO: rename this method to _make_loader(). - def _make_locator(self): - locator = Loader(search_dirs=[DATA_DIR]) - return locator - - # TODO: fully test constructor. - def test_init__reader(self): - reader = "reader" # in practice, this is a reader instance. - locator = Loader(search_dirs=None, template_locator=None, reader=reader) - - self.assertIs(locator.reader, reader) - - def _assert_template_location(self, view, expected): - locator = self._make_locator() - actual = locator.get_relative_template_location(view) - self.assertEquals(actual, expected) - - def test_get_relative_template_location(self): - """ - Test get_relative_template_location(): default behavior (no attributes set). - - """ - view = SampleView() - self._assert_template_location(view, (None, 'sample_view.mustache')) - - def test_get_relative_template_location__template_path__file_name_only(self): - """ - Test get_relative_template_location(): template_path attribute. - - """ - view = SampleView() - view.template_path = 'template.txt' - self._assert_template_location(view, ('', 'template.txt')) - - def test_get_relative_template_location__template_path__file_name_with_directory(self): - """ - Test get_relative_template_location(): template_path attribute. - - """ - view = SampleView() - view.template_path = 'foo/bar/template.txt' - self._assert_template_location(view, ('foo/bar', 'template.txt')) - - def test_get_relative_template_location__template_directory(self): - """ - Test get_relative_template_location(): template_directory attribute. - - """ - view = SampleView() - view.template_directory = 'foo' - - self._assert_template_location(view, ('foo', 'sample_view.mustache')) - - def test_get_relative_template_location__template_name(self): - """ - Test get_relative_template_location(): template_name attribute. - - """ - view = SampleView() - view.template_name = 'new_name' - self._assert_template_location(view, (None, 'new_name.mustache')) - - def test_get_relative_template_location__template_extension(self): - """ - Test get_relative_template_location(): template_extension attribute. - - """ - view = SampleView() - view.template_extension = 'txt' - self._assert_template_location(view, (None, 'sample_view.txt')) - - def test_get_template_path__with_directory(self): - """ - Test get_template_path() with a view that has a directory specified. - - """ - locator = self._make_locator() - - view = SampleView() - view.template_path = 'foo/bar.txt' - self.assertTrue(locator.get_relative_template_location(view)[0] is not None) - - actual = locator.get_template_path(view) - expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) - - self.assertEquals(actual, expected) - - def test_get_template_path__without_directory(self): - """ - Test get_template_path() with a view that doesn't have a directory specified. - - """ - locator = self._make_locator() - - view = SampleView() - self.assertTrue(locator.get_relative_template_location(view)[0] is None) - - actual = locator.get_template_path(view) - expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) - - self.assertEquals(actual, expected) - - def _assert_get_template(self, view, expected): - locator = self._make_locator() - actual = locator.get_template(view) - - self.assertEquals(type(actual), unicode) - self.assertEquals(actual, expected) - - def test_get_template(self): - """ - Test get_template(): default behavior (no attributes set). - - """ - view = SampleView() - - self._assert_get_template(view, u"ascii: abc") - - def test_get_template__template(self): - """ - Test get_template(): template attribute. - - """ - view = SampleView() - view.template = 'foo' - - self._assert_get_template(view, 'foo') - - def test_get_template__template__template_encoding(self): - """ - Test get_template(): template attribute with template encoding attribute. - - """ - view = SampleView() - view.template = u'é'.encode('utf-8') - - self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') - - view.template_encoding = 'utf-8' - self._assert_get_template(view, u'é') - - def test_get_template__template_encoding(self): - """ - Test get_template(): template_encoding attribute. - - """ - view = NonAscii() - - self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') - - view.template_encoding = 'utf-8' - self._assert_get_template(view, u"non-ascii: é") -- cgit v1.2.1 From 08b5173cf445e373426cc98e247251515dfee912 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Mar 2012 16:38:41 -0700 Subject: Renamed custom_template.Loader.get_template() to custom_template.Loader.load(). --- pystache/custom_template.py | 18 ++++++++++++------ tests/test_custom_template.py | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 40f63b9..ce594fb 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -173,14 +173,20 @@ class Loader(object): return path - def get_template(self, view): + def load(self, custom): """ - Return the unicode template string associated with a view. + Find and return the template associated to a CustomizedTemplate instance. + + Returns the template as a unicode string. + + Arguments: + + custom: a CustomizedTemplate instance. """ - if view.template is not None: - return self.reader.unicode(view.template, view.template_encoding) + if custom.template is not None: + return self.reader.unicode(custom.template, custom.template_encoding) - path = self.get_template_path(view) + path = self.get_template_path(custom) - return self.reader.read(path, view.template_encoding) + return self.reader.read(path, custom.template_encoding) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 6f21058..5db4c79 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -236,9 +236,9 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): self.assertEquals(actual, expected) - def _assert_get_template(self, view, expected): + def _assert_get_template(self, custom, expected): locator = self._make_locator() - actual = locator.get_template(view) + actual = locator.load(custom) self.assertEquals(type(actual), unicode) self.assertEquals(actual, expected) -- cgit v1.2.1 From 70b597e803d3e59ec5dc606665800ee180b32140 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 07:57:43 -0700 Subject: Changed CustomizedTemplate attributes and added custom_template.Loader tests. Added CustomizedTemplate.template_rel_path and renamed CustomizedTemplate.template_directory to CustomizedTemplate.template_rel_directory. Added unit tests to test the custom_template.Loader constructor. --- pystache/custom_template.py | 17 ++++++------ tests/test_custom_template.py | 63 ++++++++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index ce594fb..0b30470 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -29,10 +29,10 @@ class CustomizedTemplate(object): template: the template to use, as a unicode string. - template_path: the path to the template file, relative to the + template_rel_path: the path to the template file, relative to the directory containing the module defining the class. - template_dir: the directory containing the template file, relative + template_rel_directory: the directory containing the template file, relative to the directory containing the module defining the class. template_extension: the template file extension. Defaults to "mustache". @@ -41,12 +41,12 @@ class CustomizedTemplate(object): """ template = None + # TODO: remove template_path. template_path = None - - template_directory = None + template_rel_path = None + template_rel_directory = None template_name = None template_extension = None - template_encoding = None @@ -126,7 +126,6 @@ class Loader(object): """ - # TODO: unit test this. def __init__(self, search_dirs, template_locator=None, reader=None): if reader is None: reader = Reader() @@ -143,10 +142,10 @@ class Loader(object): Return the relative template path as a (dir, file_name) pair. """ - if view.template_path is not None: - return os.path.split(view.template_path) + if view.template_rel_path is not None: + return os.path.split(view.template_rel_path) - template_dir = view.template_directory + template_dir = view.template_rel_directory # Otherwise, we don't know the directory. diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 5db4c79..a159fcd 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -6,6 +6,7 @@ Unit tests of view.py. """ import os.path +import sys import unittest from examples.simple import Simple @@ -14,8 +15,9 @@ from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache import Renderer from pystache import View -from pystache.reader import Reader from pystache.custom_template import Loader +from pystache.locator import Locator +from pystache.reader import Reader from .common import AssertIsMixin from .common import DATA_DIR from .data.views import SampleView @@ -56,7 +58,7 @@ class ViewTestCase(unittest.TestCase): def test_template_path_for_partials(self): """ - Test that View.template_path is respected for partials. + Test that View.template_rel_path is respected for partials. """ class TestView(View): @@ -139,18 +141,41 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): locator = Loader(search_dirs=[DATA_DIR]) return locator - # TODO: fully test constructor. - def test_init__reader(self): - reader = "reader" # in practice, this is a reader instance. - locator = Loader(search_dirs=None, template_locator=None, reader=reader) - - self.assertIs(locator.reader, reader) - def _assert_template_location(self, view, expected): locator = self._make_locator() actual = locator.get_relative_template_location(view) self.assertEquals(actual, expected) + def test_init__defaults(self): + loader = Loader([]) + + # Check the reader attribute. + reader = loader.reader + self.assertEquals(reader.decode_errors, 'strict') + self.assertEquals(reader.encoding, sys.getdefaultencoding()) + + # Check the template_locator attribute. + locator = loader.template_locator + self.assertEquals(locator.template_extension, 'mustache') + + def test_init__search_dirs(self): + search_dirs = ['a', 'b'] + loader = Loader(search_dirs) + + self.assertEquals(loader.search_dirs, ['a', 'b']) + + def test_init__reader(self): + reader = Reader() + loader = Loader([], reader=reader) + + self.assertIs(loader.reader, reader) + + def test_init__locator(self): + locator = Locator() + loader = Loader([], template_locator=locator) + + self.assertIs(loader.template_locator, locator) + def test_get_relative_template_location(self): """ Test get_relative_template_location(): default behavior (no attributes set). @@ -159,31 +184,31 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): view = SampleView() self._assert_template_location(view, (None, 'sample_view.mustache')) - def test_get_relative_template_location__template_path__file_name_only(self): + def test_get_relative_template_location__template_rel_path__file_name_only(self): """ - Test get_relative_template_location(): template_path attribute. + Test get_relative_template_location(): template_rel_path attribute. """ view = SampleView() - view.template_path = 'template.txt' + view.template_rel_path = 'template.txt' self._assert_template_location(view, ('', 'template.txt')) - def test_get_relative_template_location__template_path__file_name_with_directory(self): + def test_get_relative_template_location__template_rel_path__file_name_with_directory(self): """ - Test get_relative_template_location(): template_path attribute. + Test get_relative_template_location(): template_rel_path attribute. """ view = SampleView() - view.template_path = 'foo/bar/template.txt' + view.template_rel_path = 'foo/bar/template.txt' self._assert_template_location(view, ('foo/bar', 'template.txt')) - def test_get_relative_template_location__template_directory(self): + def test_get_relative_template_location__template_rel_directory(self): """ - Test get_relative_template_location(): template_directory attribute. + Test get_relative_template_location(): template_rel_directory attribute. """ view = SampleView() - view.template_directory = 'foo' + view.template_rel_directory = 'foo' self._assert_template_location(view, ('foo', 'sample_view.mustache')) @@ -213,7 +238,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): locator = self._make_locator() view = SampleView() - view.template_path = 'foo/bar.txt' + view.template_rel_path = 'foo/bar.txt' self.assertTrue(locator.get_relative_template_location(view)[0] is not None) actual = locator.get_template_path(view) -- cgit v1.2.1 From d5ee38b6234d6ee9260ff8d872124b4480395067 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 12:23:38 -0700 Subject: Added some TODO's around custom_template. --- pystache/custom_template.py | 4 ++++ tests/test_custom_template.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 0b30470..a636d44 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -126,6 +126,7 @@ class Loader(object): """ + # TODO: rename template_locator to locator. def __init__(self, search_dirs, template_locator=None, reader=None): if reader is None: reader = Reader() @@ -135,8 +136,10 @@ class Loader(object): self.reader = reader self.search_dirs = search_dirs + # TODO: rename this to locator. self.template_locator = template_locator + # TODO: make this private. def get_relative_template_location(self, view): """ Return the relative template path as a (dir, file_name) pair. @@ -156,6 +159,7 @@ class Loader(object): return (template_dir, file_name) + # TODO: make this private. def get_template_path(self, view): """ Return the path to the view's associated template. diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index a159fcd..db6040d 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -134,6 +134,11 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") +# TODO: switch these tests to using the CustomizedTemplate class instead of View. +# TODO: rename the get_template() tests to test load(). +# TODO: condense, reorganize, and rename the tests so that it is +# clear whether we have full test coverage (e.g. organized by +# CustomizedTemplate attributes or something). class LoaderTests(unittest.TestCase, AssertIsMixin): # TODO: rename this method to _make_loader(). -- cgit v1.2.1 From dcb78dd65639a3fabf70a3bc463aff013b527323 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 12:26:52 -0700 Subject: Renamed custom Loader constructor parameter from template_locator to locator. --- pystache/custom_template.py | 9 ++++----- tests/test_custom_template.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index a636d44..dce4861 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -126,18 +126,17 @@ class Loader(object): """ - # TODO: rename template_locator to locator. - def __init__(self, search_dirs, template_locator=None, reader=None): + def __init__(self, search_dirs, locator=None, reader=None): if reader is None: reader = Reader() - if template_locator is None: - template_locator = TemplateLocator() + if locator is None: + locator = TemplateLocator() self.reader = reader self.search_dirs = search_dirs # TODO: rename this to locator. - self.template_locator = template_locator + self.template_locator = locator # TODO: make this private. def get_relative_template_location(self, view): diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index db6040d..c20e410 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -177,7 +177,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): def test_init__locator(self): locator = Locator() - loader = Loader([], template_locator=locator) + loader = Loader([], locator=locator) self.assertIs(loader.template_locator, locator) -- cgit v1.2.1 From 5628daddb67c2b7086f0011cb96e593cd2079655 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 12:29:58 -0700 Subject: Renamed Loader.template_locator to Loader.locator. --- pystache/custom_template.py | 11 +++++------ tests/test_custom_template.py | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index dce4861..1500014 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -135,8 +135,7 @@ class Loader(object): self.reader = reader self.search_dirs = search_dirs - # TODO: rename this to locator. - self.template_locator = locator + self.locator = locator # TODO: make this private. def get_relative_template_location(self, view): @@ -152,9 +151,9 @@ class Loader(object): # Otherwise, we don't know the directory. template_name = (view.template_name if view.template_name is not None else - self.template_locator.make_template_name(view)) + self.locator.make_template_name(view)) - file_name = self.template_locator.make_file_name(template_name, view.template_extension) + file_name = self.locator.make_file_name(template_name, view.template_extension) return (template_dir, file_name) @@ -168,9 +167,9 @@ class Loader(object): if dir_path is None: # Then we need to search for the path. - path = self.template_locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + path = self.locator.find_path_by_object(self.search_dirs, view, file_name=file_name) else: - obj_dir = self.template_locator.get_object_directory(view) + obj_dir = self.locator.get_object_directory(view) path = os.path.join(obj_dir, dir_path, file_name) return path diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index c20e410..18841bf 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -159,8 +159,8 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): self.assertEquals(reader.decode_errors, 'strict') self.assertEquals(reader.encoding, sys.getdefaultencoding()) - # Check the template_locator attribute. - locator = loader.template_locator + # Check the locator attribute. + locator = loader.locator self.assertEquals(locator.template_extension, 'mustache') def test_init__search_dirs(self): @@ -179,7 +179,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): locator = Locator() loader = Loader([], locator=locator) - self.assertIs(loader.template_locator, locator) + self.assertIs(loader.locator, locator) def test_get_relative_template_location(self): """ -- cgit v1.2.1 From 78ac3e0d1081c1fbc64247bb1c1b67cab8bb6c71 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 12:35:39 -0700 Subject: Separated LoaderTests into two test classes (as an interim step).. --- tests/test_custom_template.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 18841bf..9c93a04 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -134,22 +134,12 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -# TODO: switch these tests to using the CustomizedTemplate class instead of View. -# TODO: rename the get_template() tests to test load(). -# TODO: condense, reorganize, and rename the tests so that it is -# clear whether we have full test coverage (e.g. organized by -# CustomizedTemplate attributes or something). class LoaderTests(unittest.TestCase, AssertIsMixin): - # TODO: rename this method to _make_loader(). - def _make_locator(self): - locator = Loader(search_dirs=[DATA_DIR]) - return locator + """ + Tests the custom_template.Loader class. - def _assert_template_location(self, view, expected): - locator = self._make_locator() - actual = locator.get_relative_template_location(view) - self.assertEquals(actual, expected) + """ def test_init__defaults(self): loader = Loader([]) @@ -181,6 +171,25 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): self.assertIs(loader.locator, locator) + +# TODO: migrate these tests into the LoaderTests class. +# TODO: switch these tests to using the CustomizedTemplate class instead of View. +# TODO: rename the get_template() tests to test load(). +# TODO: condense, reorganize, and rename the tests so that it is +# clear whether we have full test coverage (e.g. organized by +# CustomizedTemplate attributes or something). +class ViewTests(unittest.TestCase): + + # TODO: rename this method to _make_loader(). + def _make_locator(self): + locator = Loader(search_dirs=[DATA_DIR]) + return locator + + def _assert_template_location(self, view, expected): + locator = self._make_locator() + actual = locator.get_relative_template_location(view) + self.assertEquals(actual, expected) + def test_get_relative_template_location(self): """ Test get_relative_template_location(): default behavior (no attributes set). -- cgit v1.2.1 From d54cc252df3472323504fcc732f3de8152f5b694 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 22 Mar 2012 12:42:11 -0700 Subject: The CustomizedTemplate tests no longer depend on the View class. --- tests/data/views.py | 6 +++--- tests/test_custom_template.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/data/views.py b/tests/data/views.py index 57b5a7d..b96d968 100644 --- a/tests/data/views.py +++ b/tests/data/views.py @@ -1,6 +1,6 @@ # coding: utf-8 -from pystache import View +from pystache import CustomizedTemplate class SayHello(object): @@ -8,9 +8,9 @@ class SayHello(object): return "World" -class SampleView(View): +class SampleView(CustomizedTemplate): pass -class NonAscii(View): +class NonAscii(CustomizedTemplate): pass diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 9c93a04..40134ae 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -173,12 +173,11 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): # TODO: migrate these tests into the LoaderTests class. -# TODO: switch these tests to using the CustomizedTemplate class instead of View. # TODO: rename the get_template() tests to test load(). # TODO: condense, reorganize, and rename the tests so that it is # clear whether we have full test coverage (e.g. organized by # CustomizedTemplate attributes or something). -class ViewTests(unittest.TestCase): +class CustomizedTemplateTests(unittest.TestCase): # TODO: rename this method to _make_loader(). def _make_locator(self): -- cgit v1.2.1 From a27c29158c9a8fe1c03abc9cc715a5f304c93ab7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Mar 2012 13:23:44 -0700 Subject: Made search_dirs an optional argument to Loader.__init__(). --- pystache/custom_template.py | 12 ++++++++---- tests/test_custom_template.py | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 1500014..a16e49a 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -13,6 +13,7 @@ from .reader import Reader from .renderer import Renderer +# TODO: rename this to Template? class CustomizedTemplate(object): """ @@ -126,16 +127,19 @@ class Loader(object): """ - def __init__(self, search_dirs, locator=None, reader=None): + def __init__(self, search_dirs=None, locator=None, reader=None): + if locator is None: + locator = TemplateLocator() + if reader is None: reader = Reader() - if locator is None: - locator = TemplateLocator() + if search_dirs is None: + search_dirs = [] + self.locator = locator self.reader = reader self.search_dirs = search_dirs - self.locator = locator # TODO: make this private. def get_relative_template_location(self, view): diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 40134ae..71b8e5d 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -13,6 +13,7 @@ from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists +from pystache import CustomizedTemplate as Template from pystache import Renderer from pystache import View from pystache.custom_template import Loader @@ -142,16 +143,19 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): """ def test_init__defaults(self): - loader = Loader([]) + loader = Loader() + + # Check the locator attribute. + locator = loader.locator + self.assertEquals(locator.template_extension, 'mustache') # Check the reader attribute. reader = loader.reader self.assertEquals(reader.decode_errors, 'strict') self.assertEquals(reader.encoding, sys.getdefaultencoding()) - # Check the locator attribute. - locator = loader.locator - self.assertEquals(locator.template_extension, 'mustache') + # Check search_dirs. + self.assertEquals(loader.search_dirs, []) def test_init__search_dirs(self): search_dirs = ['a', 'b'] @@ -171,6 +175,18 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): self.assertIs(loader.locator, locator) + def test_load__template__basic(self): + """ + Test the template attribute. + + """ + template = Template() + template.template = "abc" + + loader = Loader() + self.assertEquals(loader.load(template), "wxy") + + # TODO: migrate these tests into the LoaderTests class. # TODO: rename the get_template() tests to test load(). -- cgit v1.2.1 From 4260e313f56b242ad91cb93093c4a5fa2c64553c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Mar 2012 21:11:02 -0700 Subject: Added a Loader test case (template attribute). --- pystache/custom_template.py | 2 +- tests/common.py | 1 + tests/test_custom_template.py | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index a16e49a..52435e7 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -13,7 +13,7 @@ from .reader import Reader from .renderer import Renderer -# TODO: rename this to Template? +# TODO: consider renaming this to something like Template or TemplateInfo. class CustomizedTemplate(object): """ diff --git a/tests/common.py b/tests/common.py index 467f9af..f345630 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,6 +14,7 @@ def get_data_path(file_name): return os.path.join(DATA_DIR, file_name) +# TODO: convert this to a mixin. def assert_strings(test_case, actual, expected): # Show both friendly and literal versions. message = """\ diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 71b8e5d..ee4b8c8 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -142,6 +142,11 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): """ + def assertString(self, actual, expected): + # TODO: use the assertStrings mixin. + self.assertEquals(type(actual), type(expected)) + self.assertEquals(actual, expected) + def test_init__defaults(self): loader = Loader() @@ -184,8 +189,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin): template.template = "abc" loader = Loader() - self.assertEquals(loader.load(template), "wxy") - + self.assertString(loader.load(template), u"abc") # TODO: migrate these tests into the LoaderTests class. -- cgit v1.2.1 From eaee99c66735a7c2bb052bd14e828545f619c0b8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Mar 2012 21:30:54 -0700 Subject: Converted assert_strings() to a mixin. --- tests/common.py | 28 +++++++++++++++--------- tests/test_examples.py | 16 +++++++------- tests/test_renderengine.py | 54 +++++++++++++++++++++++----------------------- tests/test_simple.py | 6 +++--- 4 files changed, 56 insertions(+), 48 deletions(-) diff --git a/tests/common.py b/tests/common.py index f345630..cb703d7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,23 +14,31 @@ def get_data_path(file_name): return os.path.join(DATA_DIR, file_name) -# TODO: convert this to a mixin. -def assert_strings(test_case, actual, expected): - # Show both friendly and literal versions. - message = """\ +class AssertStringMixin: + """A unittest.TestCase mixin to check string equality.""" - Expected: \"""%s\""" - Actual: \"""%s\""" + def assertString(self, actual, expected): + """ + Assert that the given strings are equal and have the same type. - Expected: %s - Actual: %s""" % (expected, actual, repr(expected), repr(actual)) - test_case.assertEquals(actual, expected, message) + """ + # Show both friendly and literal versions. + message = """\ + + + Expected: \"""%s\""" + Actual: \"""%s\""" + + Expected: %s + Actual: %s""" % (expected, actual, repr(expected), repr(actual)) + self.assertEquals(actual, expected, message) + self.assertEquals(type(actual), type(expected), "Type mismatch: " + message) class AssertIsMixin: - """A mixin for adding assertIs() to a unittest.TestCase.""" + """A unittest.TestCase mixin adding assertIs().""" # unittest.assertIs() is not available until Python 2.7: # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone diff --git a/tests/test_examples.py b/tests/test_examples.py index 5c95de9..e4b41bf 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -12,21 +12,21 @@ from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext from pystache import Renderer -from tests.common import assert_strings +from tests.common import AssertStringMixin -class TestView(unittest.TestCase): +class TestView(unittest.TestCase, AssertStringMixin): def _assert(self, obj, expected): renderer = Renderer() actual = renderer.render(obj) - assert_strings(self, actual, expected) + self.assertString(actual, expected) def test_comments(self): - self._assert(Comments(), "

A Comedy of Errors

") + self._assert(Comments(), u"

A Comedy of Errors

") def test_double_section(self): - self._assert(DoubleSection(), "* first\n* second\n* third") + self._assert(DoubleSection(), u"* first\n* second\n* third") def test_unicode_output(self): self.assertEquals(UnicodeOutput().render(), u'

Name: Henri Poincaré

') @@ -36,7 +36,7 @@ class TestView(unittest.TestCase): u'

If alive today, Henri Poincaré would be 156 years old.

') def test_escaping(self): - self._assert(Escaped(), "

Bear > Shark

") + self._assert(Escaped(), u"

Bear > Shark

") def test_literal(self): self.assertEquals(Unescaped().render(), "

Bear > Shark

") @@ -48,7 +48,7 @@ Again, Welcome!""") def test_template_partial_extension(self): view = TemplatePartial() view.template_extension = 'txt' - assert_strings(self, view.render(), u"""Welcome + self.assertString(view.render(), u"""Welcome ------- ## Again, Welcome! ##""") @@ -56,7 +56,7 @@ Again, Welcome!""") def test_delimiters(self): renderer = Renderer() actual = renderer.render(Delimiters()) - assert_strings(self, actual, """\ + self.assertString(actual, u"""\ * It worked the first time. * And it worked the second time. * Then, surprisingly, it worked the third time. diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py index ccb94aa..6c2831a 100644 --- a/tests/test_renderengine.py +++ b/tests/test_renderengine.py @@ -11,7 +11,7 @@ import unittest from pystache.context import Context from pystache.parser import ParsingError from pystache.renderengine import RenderEngine -from tests.common import assert_strings +from tests.common import AssertStringMixin class RenderEngineTestCase(unittest.TestCase): @@ -31,7 +31,7 @@ class RenderEngineTestCase(unittest.TestCase): self.assertEquals(engine.load_partial, "foo") -class RenderTests(unittest.TestCase): +class RenderTests(unittest.TestCase, AssertStringMixin): """ Tests RenderEngine.render(). @@ -66,10 +66,10 @@ class RenderTests(unittest.TestCase): actual = engine.render(template, context) - assert_strings(test_case=self, actual=actual, expected=expected) + self.assertString(actual=actual, expected=expected) def test_render(self): - self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) + self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) def test__load_partial(self): """ @@ -80,7 +80,7 @@ class RenderTests(unittest.TestCase): partials = {'partial': u"{{person}}"} engine.load_partial = lambda key: partials[key] - self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) + self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) def test__literal(self): """ @@ -90,13 +90,13 @@ class RenderTests(unittest.TestCase): engine = self._engine() engine.literal = lambda s: s.upper() - self._assert_render('BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) + self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) def test_literal__sigil(self): template = "

{{& thing}}

" context = {'thing': 'Bear > Giraffe'} - expected = "

Bear > Giraffe

" + expected = u"

Bear > Giraffe

" self._assert_render(expected, template, context) @@ -108,7 +108,7 @@ class RenderTests(unittest.TestCase): engine = self._engine() engine.escape = lambda s: "**" + s - self._assert_render('**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) + self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) def test__escape_does_not_call_literal(self): """ @@ -122,7 +122,7 @@ class RenderTests(unittest.TestCase): template = 'literal: {{{foo}}} escaped: {{foo}}' context = {'foo': 'bar'} - self._assert_render('literal: BAR escaped: **bar', template, context, engine=engine) + self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine) def test__escape_preserves_unicode_subclasses(self): """ @@ -147,7 +147,7 @@ class RenderTests(unittest.TestCase): template = '{{foo1}} {{foo2}}' context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'} - self._assert_render('**bar bar**', template, context, engine=engine) + self._assert_render(u'**bar bar**', template, context, engine=engine) def test__non_basestring__literal_and_escaped(self): """ @@ -166,7 +166,7 @@ class RenderTests(unittest.TestCase): template = '{{text}} {{int}} {{{int}}}' context = {'int': 100, 'text': 'foo'} - self._assert_render('FOO 100 100', template, context, engine=engine) + self._assert_render(u'FOO 100 100', template, context, engine=engine) def test_tag__output_not_interpolated(self): """ @@ -184,7 +184,7 @@ class RenderTests(unittest.TestCase): """ template = '{{test}}' context = {'test': '{{#hello}}'} - self._assert_render('{{#hello}}', template, context) + self._assert_render(u'{{#hello}}', template, context) def test_interpolation__built_in_type__string(self): """ @@ -227,7 +227,7 @@ class RenderTests(unittest.TestCase): template = """{{#test}}{{{.}}}{{/test}}""" context = {'test': ['<', '>']} - self._assert_render('<>', template, context) + self._assert_render(u'<>', template, context) def test_implicit_iterator__escaped(self): """ @@ -237,7 +237,7 @@ class RenderTests(unittest.TestCase): template = """{{#test}}{{.}}{{/test}}""" context = {'test': ['<', '>']} - self._assert_render('<>', template, context) + self._assert_render(u'<>', template, context) def test_literal__in_section(self): """ @@ -247,7 +247,7 @@ class RenderTests(unittest.TestCase): template = '{{#test}}1 {{{less_than}}} 2{{/test}}' context = {'test': {'less_than': '<'}} - self._assert_render('1 < 2', template, context) + self._assert_render(u'1 < 2', template, context) def test_literal__in_partial(self): """ @@ -258,11 +258,11 @@ class RenderTests(unittest.TestCase): partials = {'partial': '1 {{{less_than}}} 2'} context = {'less_than': '<'} - self._assert_render('1 < 2', template, context, partials=partials) + self._assert_render(u'1 < 2', template, context, partials=partials) def test_partial(self): partials = {'partial': "{{person}}"} - self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) + self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) def test_partial__context_values(self): """ @@ -275,7 +275,7 @@ class RenderTests(unittest.TestCase): partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} context = {'foo': '<'} - self._assert_render('unescaped: < escaped: <', template, context, engine=engine, partials=partials) + self._assert_render(u'unescaped: < escaped: <', template, context, engine=engine, partials=partials) ## Test cases related specifically to sections. @@ -311,7 +311,7 @@ class RenderTests(unittest.TestCase): template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}' context = {'test': {'foo': '<'}} - self._assert_render('unescaped: < escaped: <', template, context, engine=engine) + self._assert_render(u'unescaped: < escaped: <', template, context, engine=engine) def test_section__context_precedence(self): """ @@ -338,7 +338,7 @@ class RenderTests(unittest.TestCase): template = "{{#list}}{{greeting}} {{name}}, {{/list}}" - self._assert_render("Hi Al, Hi Bob, ", template, context) + self._assert_render(u"Hi Al, Hi Bob, ", template, context) def test_section__output_not_interpolated(self): """ @@ -382,7 +382,7 @@ class RenderTests(unittest.TestCase): def test_section__lambda(self): template = '{{#test}}Mom{{/test}}' context = {'test': (lambda text: 'Hi %s' % text)} - self._assert_render('Hi Mom', template, context) + self._assert_render(u'Hi Mom', template, context) def test_section__iterable(self): """ @@ -392,10 +392,10 @@ class RenderTests(unittest.TestCase): template = '{{#iterable}}{{.}}{{/iterable}}' context = {'iterable': (i for i in range(3))} # type 'generator' - self._assert_render('012', template, context) + self._assert_render(u'012', template, context) context = {'iterable': xrange(4)} # type 'xrange' - self._assert_render('0123', template, context) + self._assert_render(u'0123', template, context) d = {'foo': 0, 'bar': 0} # We don't know what order of keys we'll be given, but from the @@ -403,7 +403,7 @@ class RenderTests(unittest.TestCase): # "If items(), keys(), values(), iteritems(), iterkeys(), and # itervalues() are called with no intervening modifications to # the dictionary, the lists will directly correspond." - expected = ''.join(d.keys()) + expected = u''.join(d.keys()) context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator' self._assert_render(expected, template, context) @@ -422,15 +422,15 @@ class RenderTests(unittest.TestCase): """ template = '{{#test}}Hi {{person}}{{/test}}' context = {'person': 'Mom', 'test': (lambda text: text + " :)")} - self._assert_render('Hi Mom :)', template, context) + self._assert_render(u'Hi Mom :)', template, context) def test_comment__multiline(self): """ Check that multiline comments are permitted. """ - self._assert_render('foobar', 'foo{{! baz }}bar') - self._assert_render('foobar', 'foo{{! \nbaz }}bar') + self._assert_render(u'foobar', 'foo{{! baz }}bar') + self._assert_render(u'foobar', 'foo{{! \nbaz }}bar') def test_custom_delimiters__sections(self): """ diff --git a/tests/test_simple.py b/tests/test_simple.py index 91661f9..3b5f188 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -8,10 +8,10 @@ from examples.lambdas import Lambdas from examples.template_partial import TemplatePartial from examples.simple import Simple -from tests.common import assert_strings +from tests.common import AssertStringMixin -class TestSimple(unittest.TestCase): +class TestSimple(unittest.TestCase, AssertStringMixin): def test_nested_context(self): view = NestedContext() @@ -62,7 +62,7 @@ class TestSimple(unittest.TestCase): """ view = TemplatePartial() view.template_extension = 'txt' - assert_strings(self, view.render(), u"""Welcome + self.assertString(view.render(), u"""Welcome ------- ## Again, Welcome! ##""") -- cgit v1.2.1 From fabc097de7009c4796434d817f04aa14ada81e40 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Mar 2012 21:32:19 -0700 Subject: LoaderTests now uses the AssertStringMixin. --- tests/test_custom_template.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index ee4b8c8..3e53190 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -20,6 +20,7 @@ from pystache.custom_template import Loader from pystache.locator import Locator from pystache.reader import Reader from .common import AssertIsMixin +from .common import AssertStringMixin from .common import DATA_DIR from .data.views import SampleView from .data.views import NonAscii @@ -135,18 +136,13 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -class LoaderTests(unittest.TestCase, AssertIsMixin): +class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): """ Tests the custom_template.Loader class. """ - def assertString(self, actual, expected): - # TODO: use the assertStrings mixin. - self.assertEquals(type(actual), type(expected)) - self.assertEquals(actual, expected) - def test_init__defaults(self): loader = Loader() -- cgit v1.2.1 From 830eaa7b77fdf3220e5570dc84047ae11998198b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Mar 2012 14:10:58 -0700 Subject: Can now pass unicode strings to Reader.unicode(). --- pystache/reader.py | 18 ++++++++++++++++-- tests/common.py | 9 ++++++--- tests/test_reader.py | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/pystache/reader.py b/pystache/reader.py index 9bb5e1d..f069d00 100644 --- a/pystache/reader.py +++ b/pystache/reader.py @@ -42,11 +42,25 @@ class Reader(object): self.decode_errors = decode_errors self.encoding = encoding - def unicode(self, text, encoding=None): + def unicode(self, s, encoding=None): + """ + Call Python's built-in function unicode(), and return the result. + + For unicode strings (or unicode subclasses), this function calls + Python's unicode() without the encoding and errors arguments. + Thus, unlike Python's built-in unicode(), it is okay to pass unicode + strings to this function. (Passing a unicode string to Python's + unicode() with the encoding argument throws the following + error: "TypeError: decoding Unicode is not supported.") + + """ + if isinstance(s, unicode): + return unicode(s) + if encoding is None: encoding = self.encoding - return unicode(text, encoding, self.decode_errors) + return unicode(s, encoding, self.decode_errors) def read(self, path, encoding=None): """ diff --git a/tests/common.py b/tests/common.py index cb703d7..603472a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -24,7 +24,7 @@ class AssertStringMixin: """ # Show both friendly and literal versions. - message = """\ + message = """String mismatch: %%s\ Expected: \"""%s\""" @@ -32,8 +32,11 @@ class AssertStringMixin: Expected: %s Actual: %s""" % (expected, actual, repr(expected), repr(actual)) - self.assertEquals(actual, expected, message) - self.assertEquals(type(actual), type(expected), "Type mismatch: " + message) + + self.assertEquals(actual, expected, message % "different characters") + + details = "types different: %s != %s" % (repr(type(expected)), repr(type(actual))) + self.assertEquals(type(expected), type(actual), message % details) class AssertIsMixin: diff --git a/tests/test_reader.py b/tests/test_reader.py index 899fda1..b4d2fb5 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -9,13 +9,14 @@ import os import sys import unittest +from .common import AssertStringMixin from pystache.reader import Reader DATA_DIR = 'tests/data' -class ReaderTestCase(unittest.TestCase): +class ReaderTestCase(unittest.TestCase, AssertStringMixin): def _get_path(self, filename): return os.path.join(DATA_DIR, filename) @@ -36,17 +37,40 @@ class ReaderTestCase(unittest.TestCase): reader = Reader(encoding='foo') self.assertEquals(reader.encoding, 'foo') - def test_unicode(self): + def test_unicode__basic__input_str(self): """ - Test unicode(): default values. + Test unicode(): default arguments with str input. """ reader = Reader() - actual = reader.unicode("foo") - self.assertEquals(type(actual), unicode) - self.assertEquals(actual, u"foo") + self.assertString(actual, u"foo") + + def test_unicode__basic__input_unicode(self): + """ + Test unicode(): default arguments with unicode input. + + """ + reader = Reader() + actual = reader.unicode(u"foo") + + self.assertString(actual, u"foo") + + def test_unicode__basic__input_unicode_subclass(self): + """ + Test unicode(): default arguments with unicode-subclass input. + + """ + class UnicodeSubclass(unicode): + pass + + s = UnicodeSubclass(u"foo") + + reader = Reader() + actual = reader.unicode(s) + + self.assertString(actual, u"foo") def test_unicode__encoding_attribute(self): """ -- cgit v1.2.1 From c2a31361f3e4e579a10a18794295ee08cb11cfa9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Mar 2012 14:27:07 -0700 Subject: More progress on LoaderTests: template attribute. --- pystache/custom_template.py | 2 +- tests/test_custom_template.py | 49 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 52435e7..31a4888 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -123,7 +123,7 @@ class View(CustomizedTemplate): class Loader(object): """ - Supports loading the template of a CustomizedTemplate instance. + Supports loading a custom-specified template. """ diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 3e53190..c03dcd7 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -139,7 +139,7 @@ class ViewTestCase(unittest.TestCase): class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): """ - Tests the custom_template.Loader class. + Tests custom_template.Loader. """ @@ -176,16 +176,51 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): self.assertIs(loader.locator, locator) - def test_load__template__basic(self): + def _assert_template(self, loader, custom, expected): + self.assertString(loader.load(custom), expected) + + def test_load__template__type_str(self): """ - Test the template attribute. + Test the template attribute: str string. """ - template = Template() - template.template = "abc" + custom = Template() + custom.template = "abc" - loader = Loader() - self.assertString(loader.load(template), u"abc") + self._assert_template(Loader(), custom, u"abc") + + def test_load__template__type_unicode(self): + """ + Test the template attribute: unicode string. + + """ + custom = Template() + custom.template = u"abc" + + self._assert_template(Loader(), custom, u"abc") + + def test_load__template__unicode_non_ascii(self): + """ + Test the template attribute: non-ascii unicode string. + + """ + custom = Template() + custom.template = u"é" + + self._assert_template(Loader(), custom, u"é") + + def test_load__template__with_template_encoding(self): + """ + Test the template attribute: with template encoding attribute. + + """ + custom = Template() + custom.template = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, self._assert_template, Loader(), custom, u'é') + + custom.template_encoding = 'utf-8' + self._assert_template(Loader(), custom, u'é') # TODO: migrate these tests into the LoaderTests class. -- cgit v1.2.1 From 2a786f00edf11689114672ff6996f036a31dc119 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Mar 2012 14:41:27 -0700 Subject: Removed two now-duplicate test cases. --- tests/test_custom_template.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index c03dcd7..12dabd2 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -341,29 +341,6 @@ class CustomizedTemplateTests(unittest.TestCase): self._assert_get_template(view, u"ascii: abc") - def test_get_template__template(self): - """ - Test get_template(): template attribute. - - """ - view = SampleView() - view.template = 'foo' - - self._assert_get_template(view, 'foo') - - def test_get_template__template__template_encoding(self): - """ - Test get_template(): template attribute with template encoding attribute. - - """ - view = SampleView() - view.template = u'é'.encode('utf-8') - - self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') - - view.template_encoding = 'utf-8' - self._assert_get_template(view, u'é') - def test_get_template__template_encoding(self): """ Test get_template(): template_encoding attribute. -- cgit v1.2.1 From 9b60fe6beb5053de7bfdc2e67451f151c71c361a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Mar 2012 14:51:01 -0700 Subject: Added a final LoaderTests unit test for the template attribute case. --- tests/test_custom_template.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 12dabd2..d67b5fc 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -222,6 +222,36 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom.template_encoding = 'utf-8' self._assert_template(Loader(), custom, u'é') + def test_load__template__correct_reader(self): + """ + Test that reader.unicode() is called with the correct arguments. + + This test is a catch-all for the template attribute in addition to + the other test cases. + + """ + class TestReader(Reader): + + def __init__(self): + self.s = None + self.encoding = None + + def unicode(self, s, encoding=None): + self.s = s + self.encoding = encoding + return u"foo" + + reader = TestReader() + loader = Loader(reader=reader) + + custom = Template() + custom.template = "template-foo" + custom.template_encoding = "encoding-foo" + + self._assert_template(loader, custom, u'foo') + self.assertEquals(reader.s, "template-foo") + self.assertEquals(reader.encoding, "encoding-foo") + # TODO: migrate these tests into the LoaderTests class. # TODO: rename the get_template() tests to test load(). -- cgit v1.2.1 From 3dbd4b9b9b990d55efd267c96fbd104f052847d8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 24 Mar 2012 14:56:16 -0700 Subject: Tweaks to the test_load__template__correct_reader() test. --- tests/test_custom_template.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index d67b5fc..02aabe4 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -224,10 +224,12 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): def test_load__template__correct_reader(self): """ - Test that reader.unicode() is called with the correct arguments. + Test that reader.unicode() is called correctly. - This test is a catch-all for the template attribute in addition to - the other test cases. + This test tests that the correct reader is called with the correct + arguments. This is a catch-all test to supplement the other + test cases. It tests Loader.load() independent of reader.unicode() + being implemented correctly (and tested). """ class TestReader(Reader): @@ -242,7 +244,8 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): return u"foo" reader = TestReader() - loader = Loader(reader=reader) + loader = Loader() + loader.reader = reader custom = Template() custom.template = "template-foo" -- cgit v1.2.1 From 5f2e47ed1adc7679630d73e4843c2a5b286c862e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Mar 2012 12:34:50 -0700 Subject: Renamed reader module to loader. --- pystache/custom_template.py | 2 +- pystache/loader.py | 73 ++++++++++++++++++++ pystache/reader.py | 73 -------------------- pystache/renderer.py | 2 +- tests/test_custom_template.py | 2 +- tests/test_loader.py | 155 ++++++++++++++++++++++++++++++++++++++++++ tests/test_locator.py | 2 +- tests/test_reader.py | 155 ------------------------------------------ 8 files changed, 232 insertions(+), 232 deletions(-) create mode 100644 pystache/loader.py delete mode 100644 pystache/reader.py create mode 100644 tests/test_loader.py delete mode 100644 tests/test_reader.py diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 31a4888..c6ce88f 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -9,7 +9,7 @@ import os.path from .context import Context from .locator import Locator as TemplateLocator -from .reader import Reader +from .loader import Reader from .renderer import Renderer diff --git a/pystache/loader.py b/pystache/loader.py new file mode 100644 index 0000000..f069d00 --- /dev/null +++ b/pystache/loader.py @@ -0,0 +1,73 @@ +# coding: utf-8 + +""" +This module provides a Reader class to read a template given a path. + +""" + +from __future__ import with_statement + +import os +import sys + + +DEFAULT_DECODE_ERRORS = 'strict' + + +class Reader(object): + + def __init__(self, encoding=None, decode_errors=None): + """ + Construct a template reader. + + Arguments: + + encoding: the file encoding. This is the name of the encoding to + use when converting file contents to unicode. This name is + passed as the encoding argument to Python's built-in function + unicode(). Defaults to the encoding name returned by + sys.getdefaultencoding(). + + decode_errors: the string to pass as the errors argument to the + built-in function unicode() when converting file contents to + unicode. Defaults to "strict". + + """ + if decode_errors is None: + decode_errors = DEFAULT_DECODE_ERRORS + + if encoding is None: + encoding = sys.getdefaultencoding() + + self.decode_errors = decode_errors + self.encoding = encoding + + def unicode(self, s, encoding=None): + """ + Call Python's built-in function unicode(), and return the result. + + For unicode strings (or unicode subclasses), this function calls + Python's unicode() without the encoding and errors arguments. + Thus, unlike Python's built-in unicode(), it is okay to pass unicode + strings to this function. (Passing a unicode string to Python's + unicode() with the encoding argument throws the following + error: "TypeError: decoding Unicode is not supported.") + + """ + if isinstance(s, unicode): + return unicode(s) + + if encoding is None: + encoding = self.encoding + + return unicode(s, encoding, self.decode_errors) + + def read(self, path, encoding=None): + """ + Read the template at the given path, and return it as a unicode string. + + """ + with open(path, 'r') as f: + text = f.read() + + return self.unicode(text, encoding) diff --git a/pystache/reader.py b/pystache/reader.py deleted file mode 100644 index f069d00..0000000 --- a/pystache/reader.py +++ /dev/null @@ -1,73 +0,0 @@ -# coding: utf-8 - -""" -This module provides a Reader class to read a template given a path. - -""" - -from __future__ import with_statement - -import os -import sys - - -DEFAULT_DECODE_ERRORS = 'strict' - - -class Reader(object): - - def __init__(self, encoding=None, decode_errors=None): - """ - Construct a template reader. - - Arguments: - - encoding: the file encoding. This is the name of the encoding to - use when converting file contents to unicode. This name is - passed as the encoding argument to Python's built-in function - unicode(). Defaults to the encoding name returned by - sys.getdefaultencoding(). - - decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting file contents to - unicode. Defaults to "strict". - - """ - if decode_errors is None: - decode_errors = DEFAULT_DECODE_ERRORS - - if encoding is None: - encoding = sys.getdefaultencoding() - - self.decode_errors = decode_errors - self.encoding = encoding - - def unicode(self, s, encoding=None): - """ - Call Python's built-in function unicode(), and return the result. - - For unicode strings (or unicode subclasses), this function calls - Python's unicode() without the encoding and errors arguments. - Thus, unlike Python's built-in unicode(), it is okay to pass unicode - strings to this function. (Passing a unicode string to Python's - unicode() with the encoding argument throws the following - error: "TypeError: decoding Unicode is not supported.") - - """ - if isinstance(s, unicode): - return unicode(s) - - if encoding is None: - encoding = self.encoding - - return unicode(s, encoding, self.decode_errors) - - def read(self, path, encoding=None): - """ - Read the template at the given path, and return it as a unicode string. - - """ - with open(path, 'r') as f: - text = f.read() - - return self.unicode(text, encoding) diff --git a/pystache/renderer.py b/pystache/renderer.py index c564162..b5f469c 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -12,7 +12,7 @@ import sys from .context import Context from .locator import DEFAULT_EXTENSION from .locator import Locator -from .reader import Reader +from .loader import Reader from .renderengine import RenderEngine diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 02aabe4..fa6505b 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -18,7 +18,7 @@ from pystache import Renderer from pystache import View from pystache.custom_template import Loader from pystache.locator import Locator -from pystache.reader import Reader +from pystache.loader import Reader from .common import AssertIsMixin from .common import AssertStringMixin from .common import DATA_DIR diff --git a/tests/test_loader.py b/tests/test_loader.py new file mode 100644 index 0000000..ff74106 --- /dev/null +++ b/tests/test_loader.py @@ -0,0 +1,155 @@ +# encoding: utf-8 + +""" +Unit tests of reader.py. + +""" + +import os +import sys +import unittest + +from .common import AssertStringMixin +from pystache.loader import Reader + + +DATA_DIR = 'tests/data' + + +class ReaderTestCase(unittest.TestCase, AssertStringMixin): + + def _get_path(self, filename): + return os.path.join(DATA_DIR, filename) + + def test_init__decode_errors(self): + # Test the default value. + reader = Reader() + self.assertEquals(reader.decode_errors, 'strict') + + reader = Reader(decode_errors='replace') + self.assertEquals(reader.decode_errors, 'replace') + + def test_init__encoding(self): + # Test the default value. + reader = Reader() + self.assertEquals(reader.encoding, sys.getdefaultencoding()) + + reader = Reader(encoding='foo') + self.assertEquals(reader.encoding, 'foo') + + def test_unicode__basic__input_str(self): + """ + Test unicode(): default arguments with str input. + + """ + reader = Reader() + actual = reader.unicode("foo") + + self.assertString(actual, u"foo") + + def test_unicode__basic__input_unicode(self): + """ + Test unicode(): default arguments with unicode input. + + """ + reader = Reader() + actual = reader.unicode(u"foo") + + self.assertString(actual, u"foo") + + def test_unicode__basic__input_unicode_subclass(self): + """ + Test unicode(): default arguments with unicode-subclass input. + + """ + class UnicodeSubclass(unicode): + pass + + s = UnicodeSubclass(u"foo") + + reader = Reader() + actual = reader.unicode(s) + + self.assertString(actual, u"foo") + + def test_unicode__encoding_attribute(self): + """ + Test unicode(): encoding attribute. + + """ + reader = Reader() + + non_ascii = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) + + reader.encoding = 'utf-8' + self.assertEquals(reader.unicode(non_ascii), u"é") + + def test_unicode__encoding_argument(self): + """ + Test unicode(): encoding argument. + + """ + reader = Reader() + + non_ascii = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) + + self.assertEquals(reader.unicode(non_ascii, encoding='utf-8'), u'é') + + def test_read(self): + """ + Test read(). + + """ + reader = Reader() + path = self._get_path('ascii.mustache') + self.assertEquals(reader.read(path), 'ascii: abc') + + def test_read__returns_unicode(self): + """ + Test that read() returns unicode strings. + + """ + reader = Reader() + path = self._get_path('ascii.mustache') + contents = reader.read(path) + self.assertEqual(type(contents), unicode) + + def test_read__encoding__attribute(self): + """ + Test read(): encoding attribute respected. + + """ + reader = Reader() + path = self._get_path('non_ascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + reader.encoding = 'utf-8' + self.assertEquals(reader.read(path), u'non-ascii: é') + + def test_read__encoding__argument(self): + """ + Test read(): encoding argument respected. + + """ + reader = Reader() + path = self._get_path('non_ascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + self.assertEquals(reader.read(path, encoding='utf-8'), u'non-ascii: é') + + def test_get__decode_errors(self): + """ + Test get(): decode_errors attribute. + + """ + reader = Reader() + path = self._get_path('non_ascii.mustache') + + self.assertRaises(UnicodeDecodeError, reader.read, path) + reader.decode_errors = 'replace' + self.assertEquals(reader.read(path), u'non-ascii: \ufffd\ufffd') + diff --git a/tests/test_locator.py b/tests/test_locator.py index d4dcc3c..0300590 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -11,7 +11,7 @@ import sys import unittest from pystache.locator import Locator -from pystache.reader import Reader +from pystache.loader import Reader from .common import DATA_DIR from data.views import SayHello diff --git a/tests/test_reader.py b/tests/test_reader.py deleted file mode 100644 index b4d2fb5..0000000 --- a/tests/test_reader.py +++ /dev/null @@ -1,155 +0,0 @@ -# encoding: utf-8 - -""" -Unit tests of reader.py. - -""" - -import os -import sys -import unittest - -from .common import AssertStringMixin -from pystache.reader import Reader - - -DATA_DIR = 'tests/data' - - -class ReaderTestCase(unittest.TestCase, AssertStringMixin): - - def _get_path(self, filename): - return os.path.join(DATA_DIR, filename) - - def test_init__decode_errors(self): - # Test the default value. - reader = Reader() - self.assertEquals(reader.decode_errors, 'strict') - - reader = Reader(decode_errors='replace') - self.assertEquals(reader.decode_errors, 'replace') - - def test_init__encoding(self): - # Test the default value. - reader = Reader() - self.assertEquals(reader.encoding, sys.getdefaultencoding()) - - reader = Reader(encoding='foo') - self.assertEquals(reader.encoding, 'foo') - - def test_unicode__basic__input_str(self): - """ - Test unicode(): default arguments with str input. - - """ - reader = Reader() - actual = reader.unicode("foo") - - self.assertString(actual, u"foo") - - def test_unicode__basic__input_unicode(self): - """ - Test unicode(): default arguments with unicode input. - - """ - reader = Reader() - actual = reader.unicode(u"foo") - - self.assertString(actual, u"foo") - - def test_unicode__basic__input_unicode_subclass(self): - """ - Test unicode(): default arguments with unicode-subclass input. - - """ - class UnicodeSubclass(unicode): - pass - - s = UnicodeSubclass(u"foo") - - reader = Reader() - actual = reader.unicode(s) - - self.assertString(actual, u"foo") - - def test_unicode__encoding_attribute(self): - """ - Test unicode(): encoding attribute. - - """ - reader = Reader() - - non_ascii = u'é'.encode('utf-8') - - self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) - - reader.encoding = 'utf-8' - self.assertEquals(reader.unicode(non_ascii), u"é") - - def test_unicode__encoding_argument(self): - """ - Test unicode(): encoding argument. - - """ - reader = Reader() - - non_ascii = u'é'.encode('utf-8') - - self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) - - self.assertEquals(reader.unicode(non_ascii, encoding='utf-8'), u'é') - - def test_read(self): - """ - Test read(). - - """ - reader = Reader() - path = self._get_path('ascii.mustache') - self.assertEquals(reader.read(path), 'ascii: abc') - - def test_read__returns_unicode(self): - """ - Test that read() returns unicode strings. - - """ - reader = Reader() - path = self._get_path('ascii.mustache') - contents = reader.read(path) - self.assertEqual(type(contents), unicode) - - def test_read__encoding__attribute(self): - """ - Test read(): encoding attribute respected. - - """ - reader = Reader() - path = self._get_path('non_ascii.mustache') - - self.assertRaises(UnicodeDecodeError, reader.read, path) - reader.encoding = 'utf-8' - self.assertEquals(reader.read(path), u'non-ascii: é') - - def test_read__encoding__argument(self): - """ - Test read(): encoding argument respected. - - """ - reader = Reader() - path = self._get_path('non_ascii.mustache') - - self.assertRaises(UnicodeDecodeError, reader.read, path) - self.assertEquals(reader.read(path, encoding='utf-8'), u'non-ascii: é') - - def test_get__decode_errors(self): - """ - Test get(): decode_errors attribute. - - """ - reader = Reader() - path = self._get_path('non_ascii.mustache') - - self.assertRaises(UnicodeDecodeError, reader.read, path) - reader.decode_errors = 'replace' - self.assertEquals(reader.read(path), u'non-ascii: \ufffd\ufffd') - -- cgit v1.2.1 From 1b18fb837f72f9e78d0a59c3f460fcdab1707ece Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Mar 2012 12:43:54 -0700 Subject: Renamed Loader to CustomLoader in test_custom_template.py. --- tests/test_custom_template.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index fa6505b..1bc1aaa 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -16,7 +16,7 @@ from examples.inverted import Inverted, InvertedLists from pystache import CustomizedTemplate as Template from pystache import Renderer from pystache import View -from pystache.custom_template import Loader +from pystache.custom_template import Loader as CustomLoader from pystache.locator import Locator from pystache.loader import Reader from .common import AssertIsMixin @@ -136,15 +136,15 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): +class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): """ - Tests custom_template.Loader. + Tests custom_template.CustomLoader. """ def test_init__defaults(self): - loader = Loader() + loader = CustomLoader() # Check the locator attribute. locator = loader.locator @@ -160,19 +160,19 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): def test_init__search_dirs(self): search_dirs = ['a', 'b'] - loader = Loader(search_dirs) + loader = CustomLoader(search_dirs) self.assertEquals(loader.search_dirs, ['a', 'b']) def test_init__reader(self): reader = Reader() - loader = Loader([], reader=reader) + loader = CustomLoader([], reader=reader) self.assertIs(loader.reader, reader) def test_init__locator(self): locator = Locator() - loader = Loader([], locator=locator) + loader = CustomLoader([], locator=locator) self.assertIs(loader.locator, locator) @@ -187,7 +187,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = "abc" - self._assert_template(Loader(), custom, u"abc") + self._assert_template(CustomLoader(), custom, u"abc") def test_load__template__type_unicode(self): """ @@ -197,7 +197,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u"abc" - self._assert_template(Loader(), custom, u"abc") + self._assert_template(CustomLoader(), custom, u"abc") def test_load__template__unicode_non_ascii(self): """ @@ -207,7 +207,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u"é" - self._assert_template(Loader(), custom, u"é") + self._assert_template(CustomLoader(), custom, u"é") def test_load__template__with_template_encoding(self): """ @@ -217,10 +217,10 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u'é'.encode('utf-8') - self.assertRaises(UnicodeDecodeError, self._assert_template, Loader(), custom, u'é') + self.assertRaises(UnicodeDecodeError, self._assert_template, CustomLoader(), custom, u'é') custom.template_encoding = 'utf-8' - self._assert_template(Loader(), custom, u'é') + self._assert_template(CustomLoader(), custom, u'é') def test_load__template__correct_reader(self): """ @@ -228,7 +228,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): This test tests that the correct reader is called with the correct arguments. This is a catch-all test to supplement the other - test cases. It tests Loader.load() independent of reader.unicode() + test cases. It tests CustomLoader.load() independent of reader.unicode() being implemented correctly (and tested). """ @@ -244,7 +244,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): return u"foo" reader = TestReader() - loader = Loader() + loader = CustomLoader() loader.reader = reader custom = Template() @@ -256,7 +256,7 @@ class LoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): self.assertEquals(reader.encoding, "encoding-foo") -# TODO: migrate these tests into the LoaderTests class. +# TODO: migrate these tests into the CustomLoaderTests class. # TODO: rename the get_template() tests to test load(). # TODO: condense, reorganize, and rename the tests so that it is # clear whether we have full test coverage (e.g. organized by @@ -265,7 +265,7 @@ class CustomizedTemplateTests(unittest.TestCase): # TODO: rename this method to _make_loader(). def _make_locator(self): - locator = Loader(search_dirs=[DATA_DIR]) + locator = CustomLoader(search_dirs=[DATA_DIR]) return locator def _assert_template_location(self, view, expected): -- cgit v1.2.1 From 9909ec904f524f341b92668ceb78d58314186ae3 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 27 Mar 2012 13:15:05 -0700 Subject: Renamed Reader class to Loader. --- pystache/custom_template.py | 3 ++- pystache/loader.py | 2 +- pystache/renderer.py | 3 ++- tests/test_custom_template.py | 3 ++- tests/test_loader.py | 3 ++- tests/test_locator.py | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index c6ce88f..3b51c28 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -9,7 +9,8 @@ import os.path from .context import Context from .locator import Locator as TemplateLocator -from .loader import Reader +# TODO: remove this alias. +from pystache.loader import Loader as Reader from .renderer import Renderer diff --git a/pystache/loader.py b/pystache/loader.py index f069d00..7c56929 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -14,7 +14,7 @@ import sys DEFAULT_DECODE_ERRORS = 'strict' -class Reader(object): +class Loader(object): def __init__(self, encoding=None, decode_errors=None): """ diff --git a/pystache/renderer.py b/pystache/renderer.py index b5f469c..92ac7ac 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -10,9 +10,10 @@ import os import sys from .context import Context +# TODO: remove this alias. +from .loader import Loader as Reader from .locator import DEFAULT_EXTENSION from .locator import Locator -from .loader import Reader from .renderengine import RenderEngine diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 1bc1aaa..2cf0abe 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -18,7 +18,8 @@ from pystache import Renderer from pystache import View from pystache.custom_template import Loader as CustomLoader from pystache.locator import Locator -from pystache.loader import Reader +# TODO: remove this alias. +from pystache.loader import Loader as Reader from .common import AssertIsMixin from .common import AssertStringMixin from .common import DATA_DIR diff --git a/tests/test_loader.py b/tests/test_loader.py index ff74106..0877f27 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,7 +10,8 @@ import sys import unittest from .common import AssertStringMixin -from pystache.loader import Reader +# TODO: remove this alias. +from pystache.loader import Loader as Reader DATA_DIR = 'tests/data' diff --git a/tests/test_locator.py b/tests/test_locator.py index 0300590..84dbf44 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -10,8 +10,9 @@ import os import sys import unittest +# TODO: remove this alias. +from pystache.loader import Loader as Reader from pystache.locator import Locator -from pystache.loader import Reader from .common import DATA_DIR from data.views import SayHello -- cgit v1.2.1 From 0009031032c6b2ab492ed067bb2b65d4bf3c5806 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Mar 2012 13:36:35 -0700 Subject: Added a defaults module with default DECODE_ERRORS and TEMPLATE_EXTENSION values. --- pystache/defaults.py | 18 ++++++++++++++++++ pystache/loader.py | 9 ++++----- pystache/locator.py | 10 +++++----- pystache/renderer.py | 9 +++++---- 4 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 pystache/defaults.py diff --git a/pystache/defaults.py b/pystache/defaults.py new file mode 100644 index 0000000..b9156d6 --- /dev/null +++ b/pystache/defaults.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +""" +This module provides a central location for defining default behavior. + +""" + +# How to handle encoding errors when decoding strings from str to unicode. +# +# This value is passed as the "errors" argument to Python's built-in +# unicode() function: +# +# http://docs.python.org/library/functions.html#unicode +# +DECODE_ERRORS = 'strict' + +# The default template extension. +TEMPLATE_EXTENSION = 'mustache' diff --git a/pystache/loader.py b/pystache/loader.py index 7c56929..2c94860 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -10,8 +10,7 @@ from __future__ import with_statement import os import sys - -DEFAULT_DECODE_ERRORS = 'strict' +from . import defaults class Loader(object): @@ -29,12 +28,12 @@ class Loader(object): sys.getdefaultencoding(). decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting file contents to - unicode. Defaults to "strict". + built-in function unicode() when converting str strings to + unicode. Defaults to the package default. """ if decode_errors is None: - decode_errors = DEFAULT_DECODE_ERRORS + decode_errors = defaults.DECODE_ERRORS if encoding is None: encoding = sys.getdefaultencoding() diff --git a/pystache/locator.py b/pystache/locator.py index ebcbd25..6bc7d64 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -9,8 +9,7 @@ import os import re import sys - -DEFAULT_EXTENSION = 'mustache' +from . import defaults class Locator(object): @@ -21,12 +20,13 @@ class Locator(object): Arguments: - extension: the template file extension. Defaults to "mustache". - Pass False for no extension (i.e. extensionless template files). + extension: the template file extension. Pass False for no + extension (i.e. to use extensionless template files). + Defaults to the package default. """ if extension is None: - extension = DEFAULT_EXTENSION + extension = defaults.TEMPLATE_EXTENSION self.template_extension = extension diff --git a/pystache/renderer.py b/pystache/renderer.py index 92ac7ac..ece6d0e 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -9,10 +9,10 @@ import cgi import os import sys +from . import defaults from .context import Context # TODO: remove this alias. from .loader import Loader as Reader -from .locator import DEFAULT_EXTENSION from .locator import Locator from .renderengine import RenderEngine @@ -96,8 +96,9 @@ class Renderer(object): current working directory. If given a string, the string is interpreted as a single directory. - file_extension: the template file extension. Defaults to "mustache". - Pass False for no extension (i.e. for extensionless files). + file_extension: the template file extension. Pass False for no + extension (i.e. to use extensionless template files). + Defaults to the package default. """ if default_encoding is None: @@ -111,7 +112,7 @@ class Renderer(object): file_encoding = default_encoding if file_extension is None: - file_extension = DEFAULT_EXTENSION + file_extension = defaults.TEMPLATE_EXTENSION if search_dirs is None: search_dirs = os.curdir # i.e. "." -- cgit v1.2.1 From c756f24366f4365d4ee6a7c39d00423f4f188d8e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Mar 2012 19:35:33 -0700 Subject: Added extension argument to Loader constructor. --- pystache/loader.py | 16 ++++++++++++---- tests/test_loader.py | 8 ++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 2c94860..968f1bd 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -15,21 +15,25 @@ from . import defaults class Loader(object): - def __init__(self, encoding=None, decode_errors=None): + def __init__(self, encoding=None, decode_errors=None, extension=None): """ Construct a template reader. Arguments: + decode_errors: the string to pass as the errors argument to the + built-in function unicode() when converting str strings to + unicode. Defaults to the package default. + encoding: the file encoding. This is the name of the encoding to use when converting file contents to unicode. This name is passed as the encoding argument to Python's built-in function unicode(). Defaults to the encoding name returned by sys.getdefaultencoding(). - decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting str strings to - unicode. Defaults to the package default. + extension: the template file extension. Pass False for no + extension (i.e. to use extensionless template files). + Defaults to the package default. """ if decode_errors is None: @@ -38,8 +42,12 @@ class Loader(object): if encoding is None: encoding = sys.getdefaultencoding() + if extension is None: + extension = defaults.TEMPLATE_EXTENSION + self.decode_errors = decode_errors self.encoding = encoding + self.extension = extension def unicode(self, s, encoding=None): """ diff --git a/tests/test_loader.py b/tests/test_loader.py index 0877f27..5e02058 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -38,6 +38,14 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): reader = Reader(encoding='foo') self.assertEquals(reader.encoding, 'foo') + def test_init__extension(self): + # Test the default value. + reader = Reader() + self.assertEquals(reader.extension, 'mustache') + + reader = Reader(extension='foo') + self.assertEquals(reader.extension, 'foo') + def test_unicode__basic__input_str(self): """ Test unicode(): default arguments with str input. -- cgit v1.2.1 From 80dd7698985949c0bb9e7995bfb5389f0328531d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Mar 2012 20:42:23 -0700 Subject: Simplified Renderer class: Renderer now uses new Loader class. --- pystache/loader.py | 24 +++++++++++++++++ pystache/renderer.py | 67 +++++++++++++-------------------------------- tests/test_loader.py | 65 +++++++++++++++++++++----------------------- tests/test_renderer.py | 73 +++++++++++--------------------------------------- 4 files changed, 88 insertions(+), 141 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 968f1bd..706a2f7 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -11,6 +11,7 @@ import os import sys from . import defaults +from .locator import Locator class Loader(object): @@ -78,3 +79,26 @@ class Loader(object): text = f.read() return self.unicode(text, encoding) + + def load(self, obj, search_dirs): + """ + Find and return the template associated to the given object. + + Arguments: + + obj: a string or object instance. If obj is a string, then obj + will be interpreted as the template name. Otherwise, obj will + be interpreted as an instance of a user-defined class. + + search_dirs: the list of directories in which to search for + templates when loading a template by name. + + """ + locator = Locator(extension=self.extension) + + if isinstance(obj, basestring): + path = locator.find_path_by_name(search_dirs, obj) + else: + path = locator.find_path_by_object(search_dirs, obj) + + return self.read(path) diff --git a/pystache/renderer.py b/pystache/renderer.py index ece6d0e..adbac3e 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -11,9 +11,7 @@ import sys from . import defaults from .context import Context -# TODO: remove this alias. -from .loader import Loader as Reader -from .locator import Locator +from .loader import Loader from .renderengine import RenderEngine @@ -43,7 +41,7 @@ class Renderer(object): """ def __init__(self, file_encoding=None, default_encoding=None, - decode_errors='strict', search_dirs=None, file_extension=None, + decode_errors=None, search_dirs=None, file_extension=None, escape=None, partials=None): """ Construct an instance. @@ -87,9 +85,8 @@ class Renderer(object): encoding name returned by sys.getdefaultencoding(). decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting to unicode any - strings of type str encountered during the rendering process. - Defaults to "strict". + built-in function unicode() when converting str strings to + unicode. Defaults to the package default. search_dirs: the list of directories in which to search for templates when loading a template by name. Defaults to the @@ -101,6 +98,9 @@ class Renderer(object): Defaults to the package default. """ + if decode_errors is None: + decode_errors = defaults.DECODE_ERRORS + if default_encoding is None: default_encoding = sys.getdefaultencoding() @@ -169,32 +169,23 @@ class Renderer(object): # the default_encoding and decode_errors attributes. return unicode(s, self.default_encoding, self.decode_errors) - def _make_reader(self): - """ - Create a Reader instance using current attributes. - - """ - return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) - - def make_locator(self): + def _make_loader(self): """ - Create a Locator instance using current attributes. + Create a Loader instance using current attributes. """ - return Locator(extension=self.file_extension) + return Loader(encoding=self.file_encoding, decode_errors=self.decode_errors, + extension=self.file_extension) def _make_load_template(self): """ Return a function that loads a template by name. """ - reader = self._make_reader() - locator = self.make_locator() + loader = self._make_loader() def load_template(template_name): - template_path = locator.find_path_by_name(self.search_dirs, template_name) - - return reader.read(template_path) + return loader.load(template_name, self.search_dirs) return load_template @@ -237,18 +228,6 @@ class Renderer(object): escape=self._escape_to_unicode) return engine - def read(self, path): - """ - Read and return as a unicode string the file contents at path. - - This class uses this method whenever it needs to read a template - file. This method uses the file_encoding and decode_errors - attributes. - - """ - reader = self._make_reader() - return reader.read(path) - # TODO: add unit tests for this method. def load_template(self, template_name): """ @@ -258,19 +237,6 @@ class Renderer(object): load_template = self._make_load_template() return load_template(template_name) - def get_associated_template(self, obj): - """ - Find and return the template associated with an object. - - The function first searches the directory containing the object's - class definition. - - """ - locator = self.make_locator() - template_path = locator.find_path_by_object(self.search_dirs, obj) - - return self.read(template_path) - def _render_string(self, template, *context, **kwargs): """ Render the given template string using the given context. @@ -291,8 +257,10 @@ class Renderer(object): Render the template associated with the given object. """ + loader = self._make_loader() + template = loader.load(obj, self.search_dirs) + context = [obj] + list(context) - template = self.get_associated_template(obj) return self._render_string(template, *context, **kwargs) @@ -303,7 +271,8 @@ class Renderer(object): Read the render() docstring for more information. """ - template = self.read(template_path) + loader = self._make_loader() + template = loader.read(template_path) return self._render_string(template, *context, **kwargs) diff --git a/tests/test_loader.py b/tests/test_loader.py index 5e02058..1836466 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,40 +10,39 @@ import sys import unittest from .common import AssertStringMixin -# TODO: remove this alias. -from pystache.loader import Loader as Reader +from pystache.loader import Loader DATA_DIR = 'tests/data' -class ReaderTestCase(unittest.TestCase, AssertStringMixin): +class LoaderTestCase(unittest.TestCase, AssertStringMixin): def _get_path(self, filename): return os.path.join(DATA_DIR, filename) def test_init__decode_errors(self): # Test the default value. - reader = Reader() + reader = Loader() self.assertEquals(reader.decode_errors, 'strict') - reader = Reader(decode_errors='replace') + reader = Loader(decode_errors='replace') self.assertEquals(reader.decode_errors, 'replace') def test_init__encoding(self): # Test the default value. - reader = Reader() + reader = Loader() self.assertEquals(reader.encoding, sys.getdefaultencoding()) - reader = Reader(encoding='foo') + reader = Loader(encoding='foo') self.assertEquals(reader.encoding, 'foo') def test_init__extension(self): # Test the default value. - reader = Reader() + reader = Loader() self.assertEquals(reader.extension, 'mustache') - reader = Reader(extension='foo') + reader = Loader(extension='foo') self.assertEquals(reader.extension, 'foo') def test_unicode__basic__input_str(self): @@ -51,7 +50,7 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): Test unicode(): default arguments with str input. """ - reader = Reader() + reader = Loader() actual = reader.unicode("foo") self.assertString(actual, u"foo") @@ -61,7 +60,7 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): Test unicode(): default arguments with unicode input. """ - reader = Reader() + reader = Loader() actual = reader.unicode(u"foo") self.assertString(actual, u"foo") @@ -76,7 +75,7 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): s = UnicodeSubclass(u"foo") - reader = Reader() + reader = Loader() actual = reader.unicode(s) self.assertString(actual, u"foo") @@ -86,7 +85,7 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): Test unicode(): encoding attribute. """ - reader = Reader() + reader = Loader() non_ascii = u'é'.encode('utf-8') @@ -100,65 +99,63 @@ class ReaderTestCase(unittest.TestCase, AssertStringMixin): Test unicode(): encoding argument. """ - reader = Reader() + reader = Loader() non_ascii = u'é'.encode('utf-8') self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) - self.assertEquals(reader.unicode(non_ascii, encoding='utf-8'), u'é') + actual = reader.unicode(non_ascii, encoding='utf-8') + self.assertEquals(actual, u'é') def test_read(self): """ Test read(). """ - reader = Reader() + reader = Loader() path = self._get_path('ascii.mustache') - self.assertEquals(reader.read(path), 'ascii: abc') - - def test_read__returns_unicode(self): - """ - Test that read() returns unicode strings. - - """ - reader = Reader() - path = self._get_path('ascii.mustache') - contents = reader.read(path) - self.assertEqual(type(contents), unicode) + actual = reader.read(path) + self.assertString(actual, u'ascii: abc') def test_read__encoding__attribute(self): """ Test read(): encoding attribute respected. """ - reader = Reader() + reader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) + reader.encoding = 'utf-8' - self.assertEquals(reader.read(path), u'non-ascii: é') + actual = reader.read(path) + self.assertString(actual, u'non-ascii: é') def test_read__encoding__argument(self): """ Test read(): encoding argument respected. """ - reader = Reader() + reader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) - self.assertEquals(reader.read(path, encoding='utf-8'), u'non-ascii: é') + + actual = reader.read(path, encoding='utf-8') + self.assertString(actual, u'non-ascii: é') def test_get__decode_errors(self): """ Test get(): decode_errors attribute. """ - reader = Reader() + reader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, reader.read, path) - reader.decode_errors = 'replace' - self.assertEquals(reader.read(path), u'non-ascii: \ufffd\ufffd') + + reader.decode_errors = 'ignore' + actual = reader.read(path) + self.assertString(actual, u'non-ascii: ') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 7a9b7cf..5c702d3 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -12,7 +12,7 @@ import unittest from examples.simple import Simple from pystache.renderer import Renderer -from pystache.locator import Locator +from pystache.loader import Loader from .common import get_data_path from .data.views import SayHello @@ -182,76 +182,33 @@ class RendererTestCase(unittest.TestCase): # U+FFFD is the official Unicode replacement character. self.assertEquals(renderer.unicode(s), u'd\ufffd\ufffdf') - ## Test the read() method. + ## Test the _make_loader() method. - def _read(self, renderer, filename): - path = get_data_path(filename) - return renderer.read(path) - - def test_read(self): - renderer = Renderer() - actual = self._read(renderer, 'ascii.mustache') - self.assertEquals(actual, 'ascii: abc') - - def test_read__returns_unicode(self): - renderer = Renderer() - actual = self._read(renderer, 'ascii.mustache') - self.assertEquals(type(actual), unicode) - - def test_read__file_encoding(self): - filename = 'non_ascii.mustache' - - renderer = Renderer() - renderer.file_encoding = 'ascii' - - self.assertRaises(UnicodeDecodeError, self._read, renderer, filename) - renderer.file_encoding = 'utf-8' - actual = self._read(renderer, filename) - self.assertEquals(actual, u'non-ascii: é') - - def test_read__decode_errors(self): - filename = 'non_ascii.mustache' - renderer = Renderer() - - self.assertRaises(UnicodeDecodeError, self._read, renderer, filename) - renderer.decode_errors = 'ignore' - actual = self._read(renderer, filename) - self.assertEquals(actual, 'non-ascii: ') - - ## Test the make_locator() method. - - def test_make_locator__return_type(self): + def test__make_loader__return_type(self): """ - Test that make_locator() returns a Locator. + Test that _make_loader() returns a Loader. """ renderer = Renderer() - locator = renderer.make_locator() + loader = renderer._make_loader() - self.assertEquals(type(locator), Locator) + self.assertEquals(type(loader), Loader) - def test_make_locator__file_extension(self): + def test__make_loader__attributes(self): """ - Test that make_locator() respects the file_extension attribute. + Test that _make_locator() sets all attributes correctly.. """ renderer = Renderer() - renderer.file_extension = 'foo' - - locator = renderer.make_locator() - - self.assertEquals(locator.template_extension, 'foo') - - # This test is a sanity check. Strictly speaking, it shouldn't - # be necessary based on our tests above. - def test_make_locator__default(self): - renderer = Renderer() - actual = renderer.make_locator() + renderer.decode_errors = 'dec' + renderer.file_encoding = 'enc' + renderer.file_extension = 'ext' - expected = Locator() + loader = renderer._make_loader() - self.assertEquals(type(actual), type(expected)) - self.assertEquals(actual.template_extension, expected.template_extension) + self.assertEquals(loader.decode_errors, 'dec') + self.assertEquals(loader.encoding, 'enc') + self.assertEquals(loader.extension, 'ext') ## Test the render() method. -- cgit v1.2.1 From ed2c5521ec33bdad720d53dcaf41c1a19a276490 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 28 Mar 2012 22:40:16 -0700 Subject: Divided Loader.load() into load_name() and load_object(). --- pystache/loader.py | 34 ++++++++++++++++++++++++---------- pystache/renderer.py | 4 ++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 706a2f7..fb30857 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -50,6 +50,7 @@ class Loader(object): self.encoding = encoding self.extension = extension + # TODO: eliminate redundancy with the Renderer class's unicode code. def unicode(self, s, encoding=None): """ Call Python's built-in function unicode(), and return the result. @@ -80,25 +81,38 @@ class Loader(object): return self.unicode(text, encoding) - def load(self, obj, search_dirs): + # TODO: unit-test this method. + def load_name(self, name, search_dirs): + """ + Find and return the template with the given name. + + Arguments: + + name: the name of the template. + + search_dirs: the list of directories in which to search. + + """ + locator = Locator(extension=self.extension) + + path = locator.find_path_by_name(search_dirs, name) + + return self.read(path) + + # TODO: unit-test this method. + def load_object(self, obj, search_dirs): """ Find and return the template associated to the given object. Arguments: - obj: a string or object instance. If obj is a string, then obj - will be interpreted as the template name. Otherwise, obj will - be interpreted as an instance of a user-defined class. + obj: an instance of a user-defined class. - search_dirs: the list of directories in which to search for - templates when loading a template by name. + search_dirs: the list of directories in which to search. """ locator = Locator(extension=self.extension) - if isinstance(obj, basestring): - path = locator.find_path_by_name(search_dirs, obj) - else: - path = locator.find_path_by_object(search_dirs, obj) + path = locator.find_path_by_object(search_dirs, obj) return self.read(path) diff --git a/pystache/renderer.py b/pystache/renderer.py index adbac3e..ffc3194 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -185,7 +185,7 @@ class Renderer(object): loader = self._make_loader() def load_template(template_name): - return loader.load(template_name, self.search_dirs) + return loader.load_name(template_name, self.search_dirs) return load_template @@ -258,7 +258,7 @@ class Renderer(object): """ loader = self._make_loader() - template = loader.load(obj, self.search_dirs) + template = loader.load_object(obj, self.search_dirs) context = [obj] + list(context) -- cgit v1.2.1 From cab8528f0ab06513a35dbb635b68b0d509c18e44 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Mar 2012 01:05:11 -0700 Subject: Renamed custom_template.Loader to CustomLoader. --- pystache/custom_template.py | 7 +++---- tests/test_custom_template.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 3b51c28..53d46f5 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -8,9 +8,8 @@ This module supports specifying custom template information per view. import os.path from .context import Context +from .loader import Loader from .locator import Locator as TemplateLocator -# TODO: remove this alias. -from pystache.loader import Loader as Reader from .renderer import Renderer @@ -121,7 +120,7 @@ class View(CustomizedTemplate): return renderer.render(template, self.context) -class Loader(object): +class CustomLoader(object): """ Supports loading a custom-specified template. @@ -133,7 +132,7 @@ class Loader(object): locator = TemplateLocator() if reader is None: - reader = Reader() + reader = Loader() if search_dirs is None: search_dirs = [] diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 2cf0abe..2c1a714 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -16,7 +16,7 @@ from examples.inverted import Inverted, InvertedLists from pystache import CustomizedTemplate as Template from pystache import Renderer from pystache import View -from pystache.custom_template import Loader as CustomLoader +from pystache.custom_template import CustomLoader from pystache.locator import Locator # TODO: remove this alias. from pystache.loader import Loader as Reader -- cgit v1.2.1 From d22cc3b06da964eba0928c8035ec8cfcede37f01 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 29 Mar 2012 08:42:26 -0700 Subject: Removed the reader argument from CustomLoader's constructor. --- pystache/custom_template.py | 34 ++++++++++++++------------ tests/test_custom_template.py | 57 +++++++++++++++++++------------------------ 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index 53d46f5..c3f232d 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -9,7 +9,7 @@ import os.path from .context import Context from .loader import Loader -from .locator import Locator as TemplateLocator +from .locator import Locator from .renderer import Renderer @@ -56,7 +56,7 @@ class View(CustomizedTemplate): _renderer = None - locator = TemplateLocator() + locator = Locator() def __init__(self, context=None): """ @@ -127,18 +127,14 @@ class CustomLoader(object): """ - def __init__(self, search_dirs=None, locator=None, reader=None): - if locator is None: - locator = TemplateLocator() - - if reader is None: - reader = Loader() + def __init__(self, search_dirs=None, loader=None): + if loader is None: + loader = Loader() if search_dirs is None: search_dirs = [] - self.locator = locator - self.reader = reader + self.loader = loader self.search_dirs = search_dirs # TODO: make this private. @@ -154,10 +150,13 @@ class CustomLoader(object): # Otherwise, we don't know the directory. + # TODO: share code with the loader attribute here. + locator = Locator(extension=self.loader.extension) + template_name = (view.template_name if view.template_name is not None else - self.locator.make_template_name(view)) + locator.make_template_name(view)) - file_name = self.locator.make_file_name(template_name, view.template_extension) + file_name = locator.make_file_name(template_name, view.template_extension) return (template_dir, file_name) @@ -169,11 +168,14 @@ class CustomLoader(object): """ dir_path, file_name = self.get_relative_template_location(view) + # TODO: share code with the loader attribute here. + locator = Locator(extension=self.loader.extension) + if dir_path is None: # Then we need to search for the path. - path = self.locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + path = locator.find_path_by_object(self.search_dirs, view, file_name=file_name) else: - obj_dir = self.locator.get_object_directory(view) + obj_dir = locator.get_object_directory(view) path = os.path.join(obj_dir, dir_path, file_name) return path @@ -190,8 +192,8 @@ class CustomLoader(object): """ if custom.template is not None: - return self.reader.unicode(custom.template, custom.template_encoding) + return self.loader.unicode(custom.template, custom.template_encoding) path = self.get_template_path(custom) - return self.reader.read(path, custom.template_encoding) + return self.loader.read(path, custom.template_encoding) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index 2c1a714..d46b5bf 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -18,8 +18,7 @@ from pystache import Renderer from pystache import View from pystache.custom_template import CustomLoader from pystache.locator import Locator -# TODO: remove this alias. -from pystache.loader import Loader as Reader +from pystache.loader import Loader from .common import AssertIsMixin from .common import AssertStringMixin from .common import DATA_DIR @@ -145,19 +144,15 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): """ def test_init__defaults(self): - loader = CustomLoader() - - # Check the locator attribute. - locator = loader.locator - self.assertEquals(locator.template_extension, 'mustache') + custom = CustomLoader() # Check the reader attribute. - reader = loader.reader - self.assertEquals(reader.decode_errors, 'strict') - self.assertEquals(reader.encoding, sys.getdefaultencoding()) + loader = custom.loader + self.assertEquals(loader.decode_errors, 'strict') + self.assertEquals(loader.encoding, sys.getdefaultencoding()) # Check search_dirs. - self.assertEquals(loader.search_dirs, []) + self.assertEquals(custom.search_dirs, []) def test_init__search_dirs(self): search_dirs = ['a', 'b'] @@ -165,18 +160,13 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): self.assertEquals(loader.search_dirs, ['a', 'b']) - def test_init__reader(self): - reader = Reader() - loader = CustomLoader([], reader=reader) - - self.assertIs(loader.reader, reader) + def test_init__loader(self): + loader = Loader() + custom = CustomLoader([], loader=loader) - def test_init__locator(self): - locator = Locator() - loader = CustomLoader([], locator=locator) - - self.assertIs(loader.locator, locator) + self.assertIs(custom.loader, loader) + # TODO: rename to something like _assert_load(). def _assert_template(self, loader, custom, expected): self.assertString(loader.load(custom), expected) @@ -223,7 +213,8 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom.template_encoding = 'utf-8' self._assert_template(CustomLoader(), custom, u'é') - def test_load__template__correct_reader(self): + # TODO: make this test complete. + def test_load__template__correct_loader(self): """ Test that reader.unicode() is called correctly. @@ -233,28 +224,30 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): being implemented correctly (and tested). """ - class TestReader(Reader): + class MockLoader(Loader): def __init__(self): self.s = None self.encoding = None + # Overrides the existing method. def unicode(self, s, encoding=None): self.s = s self.encoding = encoding return u"foo" - reader = TestReader() - loader = CustomLoader() - loader.reader = reader + loader = MockLoader() + custom_loader = CustomLoader() + custom_loader.loader = loader - custom = Template() - custom.template = "template-foo" - custom.template_encoding = "encoding-foo" + view = Template() + view.template = "template-foo" + view.template_encoding = "encoding-foo" - self._assert_template(loader, custom, u'foo') - self.assertEquals(reader.s, "template-foo") - self.assertEquals(reader.encoding, "encoding-foo") + # Check that our unicode() above was called. + self._assert_template(custom_loader, view, u'foo') + self.assertEquals(loader.s, "template-foo") + self.assertEquals(loader.encoding, "encoding-foo") # TODO: migrate these tests into the CustomLoaderTests class. -- cgit v1.2.1 From cdbfdf0a394e058e89bcc3f606f5ddeffd95228d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 30 Mar 2012 18:41:02 -0700 Subject: Refactored the Loader class to use the Renderer class's unicode. As a result, it is no longer necessary to pass decode_errors to the Loader class. --- pystache/custom_template.py | 3 ++ pystache/defaults.py | 27 ++++++++++ pystache/loader.py | 87 ++++++++++++++++++------------ pystache/renderer.py | 27 +++++----- tests/test_custom_template.py | 7 +-- tests/test_loader.py | 119 +++++++++++++++++++++++++++--------------- tests/test_renderer.py | 10 ++-- 7 files changed, 185 insertions(+), 95 deletions(-) diff --git a/pystache/custom_template.py b/pystache/custom_template.py index c3f232d..5f6a79b 100644 --- a/pystache/custom_template.py +++ b/pystache/custom_template.py @@ -120,6 +120,9 @@ class View(CustomizedTemplate): return renderer.render(template, self.context) +# TODO: finalize this class's name. +# TODO: get this class fully working with test cases, and then refactor +# and replace the View class. class CustomLoader(object): """ diff --git a/pystache/defaults.py b/pystache/defaults.py index b9156d6..69c4995 100644 --- a/pystache/defaults.py +++ b/pystache/defaults.py @@ -3,8 +3,15 @@ """ This module provides a central location for defining default behavior. +Throughout the package, these defaults take effect only when the user +does not otherwise specify a value. + """ +import cgi +import sys + + # How to handle encoding errors when decoding strings from str to unicode. # # This value is passed as the "errors" argument to Python's built-in @@ -14,5 +21,25 @@ This module provides a central location for defining default behavior. # DECODE_ERRORS = 'strict' +# The name of the encoding to use when converting to unicode any strings of +# type str encountered during the rendering process. +STRING_ENCODING = sys.getdefaultencoding() + +# The name of the encoding to use when converting file contents to unicode. +# This default takes precedence over the STRING_ENCODING default for +# strings that arise from files. +FILE_ENCODING = sys.getdefaultencoding() + +# The escape function to apply to strings that require escaping when +# rendering templates (e.g. for tags enclosed in double braces). +# Only unicode strings will be passed to this function. +# +# The quote=True argument causes double quotes to be escaped, +# but not single quotes: +# +# http://docs.python.org/library/cgi.html#cgi.escape +# +TAG_ESCAPE = lambda u: cgi.escape(u, quote=True) + # The default template extension. TEMPLATE_EXTENSION = 'mustache' diff --git a/pystache/loader.py b/pystache/loader.py index fb30857..d7cdca1 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module provides a Reader class to read a template given a path. +This module provides a Loader class for locating and reading templates. """ @@ -14,62 +14,82 @@ from . import defaults from .locator import Locator +def _to_unicode(s, encoding=None): + """ + Raises a TypeError exception if the given string is already unicode. + + """ + if encoding is None: + encoding = defaults.STRING_ENCODING + return unicode(s, encoding, defaults.DECODE_ERRORS) + + class Loader(object): - def __init__(self, encoding=None, decode_errors=None, extension=None): - """ - Construct a template reader. + """ + Loads the template associated to a name or user-defined object. - Arguments: + """ - decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting str strings to - unicode. Defaults to the package default. + def __init__(self, file_encoding=None, extension=None, to_unicode=None): + """ + Construct a template loader instance. - encoding: the file encoding. This is the name of the encoding to - use when converting file contents to unicode. This name is - passed as the encoding argument to Python's built-in function - unicode(). Defaults to the encoding name returned by - sys.getdefaultencoding(). + Arguments: extension: the template file extension. Pass False for no extension (i.e. to use extensionless template files). Defaults to the package default. - """ - if decode_errors is None: - decode_errors = defaults.DECODE_ERRORS + file_encoding: the name of the encoding to use when converting file + contents to unicode. Defaults to the package default. - if encoding is None: - encoding = sys.getdefaultencoding() + to_unicode: the function to use when converting strings of type + str to unicode. The function should have the signature: + + to_unicode(s, encoding=None) + + It should accept a string of type str and an optional encoding + name and return a string of type unicode. Defaults to calling + Python's built-in function unicode() using the package encoding + and decode-errors defaults. + """ if extension is None: extension = defaults.TEMPLATE_EXTENSION - self.decode_errors = decode_errors - self.encoding = encoding + if file_encoding is None: + file_encoding = defaults.FILE_ENCODING + + if to_unicode is None: + to_unicode = _to_unicode + self.extension = extension + self.file_encoding = file_encoding + self.to_unicode = to_unicode - # TODO: eliminate redundancy with the Renderer class's unicode code. def unicode(self, s, encoding=None): """ - Call Python's built-in function unicode(), and return the result. + Convert a string to unicode using the given encoding, and return it. + + This function uses the underlying to_unicode attribute. + + Arguments: - For unicode strings (or unicode subclasses), this function calls - Python's unicode() without the encoding and errors arguments. - Thus, unlike Python's built-in unicode(), it is okay to pass unicode - strings to this function. (Passing a unicode string to Python's - unicode() with the encoding argument throws the following - error: "TypeError: decoding Unicode is not supported.") + s: a basestring instance to convert to unicode. Unlike Python's + built-in unicode() function, it is okay to pass unicode strings + to this function. (Passing a unicode string to Python's unicode() + with the encoding argument throws the error, "TypeError: decoding + Unicode is not supported.") + + encoding: the encoding to pass to the to_unicode attribute. + Defaults to None. """ if isinstance(s, unicode): return unicode(s) - if encoding is None: - encoding = self.encoding - - return unicode(s, encoding, self.decode_errors) + return self.to_unicode(s, encoding) def read(self, path, encoding=None): """ @@ -79,6 +99,9 @@ class Loader(object): with open(path, 'r') as f: text = f.read() + if encoding is None: + encoding = self.file_encoding + return self.unicode(text, encoding) # TODO: unit-test this method. diff --git a/pystache/renderer.py b/pystache/renderer.py index ffc3194..9ed0949 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -5,9 +5,7 @@ This module provides a Renderer class to render templates. """ -import cgi import os -import sys from . import defaults from .context import Context @@ -15,12 +13,6 @@ from .loader import Loader from .renderengine import RenderEngine -# The quote=True argument causes double quotes to be escaped, -# but not single quotes: -# http://docs.python.org/library/cgi.html#cgi.escape -DEFAULT_ESCAPE = lambda s: cgi.escape(s, quote=True) - - class Renderer(object): """ @@ -40,6 +32,7 @@ class Renderer(object): """ + # TODO: rename default_encoding to string_encoding. def __init__(self, file_encoding=None, default_encoding=None, decode_errors=None, search_dirs=None, file_extension=None, escape=None, partials=None): @@ -82,7 +75,7 @@ class Renderer(object): to unicode any strings of type str encountered during the rendering process. The name will be passed as the encoding argument to the built-in function unicode(). Defaults to the - encoding name returned by sys.getdefaultencoding(). + package default. decode_errors: the string to pass as the errors argument to the built-in function unicode() when converting str strings to @@ -102,10 +95,10 @@ class Renderer(object): decode_errors = defaults.DECODE_ERRORS if default_encoding is None: - default_encoding = sys.getdefaultencoding() + default_encoding = defaults.STRING_ENCODING if escape is None: - escape = DEFAULT_ESCAPE + escape = defaults.TAG_ESCAPE # This needs to be after we set the default default_encoding. if file_encoding is None: @@ -121,6 +114,7 @@ class Renderer(object): search_dirs = [search_dirs] self.decode_errors = decode_errors + # TODO: rename this attribute to string_encoding. self.default_encoding = default_encoding self.escape = escape self.file_encoding = file_encoding @@ -152,7 +146,7 @@ class Renderer(object): """ return unicode(self.escape(self._to_unicode_soft(s))) - def unicode(self, s): + def unicode(self, s, encoding=None): """ Convert a string to unicode, using default_encoding and decode_errors. @@ -165,17 +159,20 @@ class Renderer(object): TypeError: decoding Unicode is not supported """ + if encoding is None: + encoding = self.default_encoding + # TODO: Wrap UnicodeDecodeErrors with a message about setting # the default_encoding and decode_errors attributes. - return unicode(s, self.default_encoding, self.decode_errors) + return unicode(s, encoding, self.decode_errors) def _make_loader(self): """ Create a Loader instance using current attributes. """ - return Loader(encoding=self.file_encoding, decode_errors=self.decode_errors, - extension=self.file_extension) + return Loader(file_encoding=self.file_encoding, extension=self.file_extension, + to_unicode=self.unicode) def _make_load_template(self): """ diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py index d46b5bf..eedf431 100644 --- a/tests/test_custom_template.py +++ b/tests/test_custom_template.py @@ -146,10 +146,11 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): def test_init__defaults(self): custom = CustomLoader() - # Check the reader attribute. + # Check the loader attribute. loader = custom.loader - self.assertEquals(loader.decode_errors, 'strict') - self.assertEquals(loader.encoding, sys.getdefaultencoding()) + self.assertEquals(loader.extension, 'mustache') + self.assertEquals(loader.file_encoding, sys.getdefaultencoding()) + to_unicode = loader.to_unicode # Check search_dirs. self.assertEquals(custom.search_dirs, []) diff --git a/tests/test_loader.py b/tests/test_loader.py index 1836466..a285e68 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,40 +10,71 @@ import sys import unittest from .common import AssertStringMixin +from pystache import defaults from pystache.loader import Loader DATA_DIR = 'tests/data' -class LoaderTestCase(unittest.TestCase, AssertStringMixin): - - def _get_path(self, filename): - return os.path.join(DATA_DIR, filename) - - def test_init__decode_errors(self): - # Test the default value. - reader = Loader() - self.assertEquals(reader.decode_errors, 'strict') - - reader = Loader(decode_errors='replace') - self.assertEquals(reader.decode_errors, 'replace') - - def test_init__encoding(self): - # Test the default value. - reader = Loader() - self.assertEquals(reader.encoding, sys.getdefaultencoding()) - - reader = Loader(encoding='foo') - self.assertEquals(reader.encoding, 'foo') +class LoaderTests(unittest.TestCase, AssertStringMixin): def test_init__extension(self): + loader = Loader(extension='foo') + self.assertEquals(loader.extension, 'foo') + + def test_init__extension__default(self): # Test the default value. - reader = Loader() - self.assertEquals(reader.extension, 'mustache') + loader = Loader() + self.assertEquals(loader.extension, 'mustache') + + def test_init__file_encoding(self): + loader = Loader(file_encoding='bar') + self.assertEquals(loader.file_encoding, 'bar') + + def test_init__file_encoding__default(self): + file_encoding = defaults.FILE_ENCODING + try: + defaults.FILE_ENCODING = 'foo' + loader = Loader() + self.assertEquals(loader.file_encoding, 'foo') + finally: + defaults.FILE_ENCODING = file_encoding + + def test_init__to_unicode(self): + to_unicode = lambda x: x + loader = Loader(to_unicode=to_unicode) + self.assertEquals(loader.to_unicode, to_unicode) + + def test_init__to_unicode__default(self): + loader = Loader() + self.assertRaises(TypeError, loader.to_unicode, u"abc") + + decode_errors = defaults.DECODE_ERRORS + string_encoding = defaults.STRING_ENCODING + + nonascii = 'abcdé' + + try: + defaults.DECODE_ERRORS = 'strict' + defaults.STRING_ENCODING = 'ascii' + loader = Loader() + self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii) + + defaults.DECODE_ERRORS = 'ignore' + loader = Loader() + self.assertString(loader.to_unicode(nonascii), u'abcd') + + defaults.STRING_ENCODING = 'utf-8' + loader = Loader() + self.assertString(loader.to_unicode(nonascii), u'abcdé') + + finally: + defaults.DECODE_ERRORS = decode_errors + defaults.STRING_ENCODING = string_encoding - reader = Loader(extension='foo') - self.assertEquals(reader.extension, 'foo') + def _get_path(self, filename): + return os.path.join(DATA_DIR, filename) def test_unicode__basic__input_str(self): """ @@ -80,19 +111,24 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): self.assertString(actual, u"foo") - def test_unicode__encoding_attribute(self): + def test_unicode__to_unicode__attribute(self): """ Test unicode(): encoding attribute. """ reader = Loader() - non_ascii = u'é'.encode('utf-8') + non_ascii = u'abcdé'.encode('utf-8') self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) - reader.encoding = 'utf-8' - self.assertEquals(reader.unicode(non_ascii), u"é") + def to_unicode(s, encoding=None): + if encoding is None: + encoding = 'utf-8' + return unicode(s, encoding) + + reader.to_unicode = to_unicode + self.assertString(reader.unicode(non_ascii), u"abcdé") def test_unicode__encoding_argument(self): """ @@ -101,13 +137,14 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): """ reader = Loader() - non_ascii = u'é'.encode('utf-8') + non_ascii = u'abcdé'.encode('utf-8') self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii) actual = reader.unicode(non_ascii, encoding='utf-8') - self.assertEquals(actual, u'é') + self.assertString(actual, u'abcdé') + # TODO: check the read() unit tests. def test_read(self): """ Test read(). @@ -118,18 +155,18 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): actual = reader.read(path) self.assertString(actual, u'ascii: abc') - def test_read__encoding__attribute(self): + def test_read__file_encoding__attribute(self): """ - Test read(): encoding attribute respected. + Test read(): file_encoding attribute respected. """ - reader = Loader() + loader = Loader() path = self._get_path('non_ascii.mustache') - self.assertRaises(UnicodeDecodeError, reader.read, path) + self.assertRaises(UnicodeDecodeError, loader.read, path) - reader.encoding = 'utf-8' - actual = reader.read(path) + loader.file_encoding = 'utf-8' + actual = loader.read(path) self.assertString(actual, u'non-ascii: é') def test_read__encoding__argument(self): @@ -145,9 +182,9 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): actual = reader.read(path, encoding='utf-8') self.assertString(actual, u'non-ascii: é') - def test_get__decode_errors(self): + def test_reader__to_unicode__attribute(self): """ - Test get(): decode_errors attribute. + Test read(): to_unicode attribute respected. """ reader = Loader() @@ -155,7 +192,7 @@ class LoaderTestCase(unittest.TestCase, AssertStringMixin): self.assertRaises(UnicodeDecodeError, reader.read, path) - reader.decode_errors = 'ignore' - actual = reader.read(path) - self.assertString(actual, u'non-ascii: ') + #reader.decode_errors = 'ignore' + #actual = reader.read(path) + #self.assertString(actual, u'non-ascii: ') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 5c702d3..49f3999 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -196,19 +196,21 @@ class RendererTestCase(unittest.TestCase): def test__make_loader__attributes(self): """ - Test that _make_locator() sets all attributes correctly.. + Test that _make_loader() sets all attributes correctly.. """ + unicode_ = lambda x: x + renderer = Renderer() - renderer.decode_errors = 'dec' renderer.file_encoding = 'enc' renderer.file_extension = 'ext' + renderer.unicode = unicode_ loader = renderer._make_loader() - self.assertEquals(loader.decode_errors, 'dec') - self.assertEquals(loader.encoding, 'enc') self.assertEquals(loader.extension, 'ext') + self.assertEquals(loader.file_encoding, 'enc') + self.assertEquals(loader.to_unicode, unicode_) ## Test the render() method. -- cgit v1.2.1 From e4c083f6fdd8c132bdcaeb4ff9466cc81acd9318 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 30 Mar 2012 18:44:38 -0700 Subject: Added to README advice on how to run a subset of unit tests. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 1753046..b83e0be 100644 --- a/README.rst +++ b/README.rst @@ -88,6 +88,9 @@ or alternatively (using setup.cfg):: python setup.py nosetests +To run a subset of the tests, you can use this pattern, for example: :: + + nosetests --tests tests/test_context.py:GetValueTests.test_dictionary__key_present Mailing List ================== -- cgit v1.2.1 From 85f5ef0f8717f53260cdeeb8f457a1fc0fab6f40 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 30 Mar 2012 19:09:47 -0700 Subject: Renamed Renderer.default_encoding to Renderer.string_encoding. --- pystache/renderer.py | 34 +++++++++++++++++----------------- tests/test_renderer.py | 36 ++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 9ed0949..4a7b11d 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -32,8 +32,8 @@ class Renderer(object): """ - # TODO: rename default_encoding to string_encoding. - def __init__(self, file_encoding=None, default_encoding=None, + # TODO: file_encoding should default to the package default. + def __init__(self, file_encoding=None, string_encoding=None, decode_errors=None, search_dirs=None, file_extension=None, escape=None, partials=None): """ @@ -66,12 +66,13 @@ class Renderer(object): escape function, for example. One may also wish to consider using markupsafe's escape function: markupsafe.escape(). - file_encoding: the name of the encoding of all template files. - This encoding is used when reading and converting any template - files to unicode. All templates are converted to unicode prior - to parsing. Defaults to the default_encoding argument. + file_encoding: the name of the default encoding to use when reading + template files. All templates are converted to unicode prior + to parsing. This encoding is used when reading template files + and converting them to unicode. Defaults to the value of the + string_encoding argument. - default_encoding: the name of the encoding to use when converting + string_encoding: the name of the encoding to use when converting to unicode any strings of type str encountered during the rendering process. The name will be passed as the encoding argument to the built-in function unicode(). Defaults to the @@ -94,15 +95,15 @@ class Renderer(object): if decode_errors is None: decode_errors = defaults.DECODE_ERRORS - if default_encoding is None: - default_encoding = defaults.STRING_ENCODING + if string_encoding is None: + string_encoding = defaults.STRING_ENCODING if escape is None: escape = defaults.TAG_ESCAPE - # This needs to be after we set the default default_encoding. + # This needs to be after we set the default string_encoding. if file_encoding is None: - file_encoding = default_encoding + file_encoding = string_encoding if file_extension is None: file_extension = defaults.TEMPLATE_EXTENSION @@ -114,8 +115,7 @@ class Renderer(object): search_dirs = [search_dirs] self.decode_errors = decode_errors - # TODO: rename this attribute to string_encoding. - self.default_encoding = default_encoding + self.string_encoding = string_encoding self.escape = escape self.file_encoding = file_encoding self.file_extension = file_extension @@ -148,7 +148,7 @@ class Renderer(object): def unicode(self, s, encoding=None): """ - Convert a string to unicode, using default_encoding and decode_errors. + Convert a string to unicode, using string_encoding and decode_errors. Raises: @@ -160,10 +160,10 @@ class Renderer(object): """ if encoding is None: - encoding = self.default_encoding + encoding = self.string_encoding # TODO: Wrap UnicodeDecodeErrors with a message about setting - # the default_encoding and decode_errors attributes. + # the string_encoding and decode_errors attributes. return unicode(s, encoding, self.decode_errors) def _make_loader(self): @@ -280,7 +280,7 @@ class Renderer(object): Returns the rendering as a unicode string. Prior to rendering, templates of type str are converted to unicode - using the default_encoding and decode_errors attributes. See the + using the string_encoding and decode_errors attributes. See the constructor docstring for more information. Arguments: diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 49f3999..c256384 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -54,21 +54,21 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer(escape=escape) self.assertEquals(renderer.escape("bar"), "**bar") - def test_default_encoding__default(self): + def test_string_encoding__default(self): """ Check the default value. """ renderer = Renderer() - self.assertEquals(renderer.default_encoding, sys.getdefaultencoding()) + self.assertEquals(renderer.string_encoding, sys.getdefaultencoding()) - def test_default_encoding(self): + def test_string_encoding(self): """ Check that the constructor sets the attribute correctly. """ - renderer = Renderer(default_encoding="foo") - self.assertEquals(renderer.default_encoding, "foo") + renderer = Renderer(string_encoding="foo") + self.assertEquals(renderer.string_encoding, "foo") def test_decode_errors__default(self): """ @@ -92,7 +92,7 @@ class RendererInitTestCase(unittest.TestCase): """ renderer = Renderer() - self.assertEquals(renderer.file_encoding, renderer.default_encoding) + self.assertEquals(renderer.file_encoding, renderer.string_encoding) def test_file_encoding(self): """ @@ -152,18 +152,18 @@ class RendererTestCase(unittest.TestCase): ## Test Renderer.unicode(). - def test_unicode__default_encoding(self): + def test_unicode__string_encoding(self): """ - Test that the default_encoding attribute is respected. + Test that the string_encoding attribute is respected. """ renderer = Renderer() s = "é" - renderer.default_encoding = "ascii" + renderer.string_encoding = "ascii" self.assertRaises(UnicodeDecodeError, renderer.unicode, s) - renderer.default_encoding = "utf-8" + renderer.string_encoding = "utf-8" self.assertEquals(renderer.unicode(s), u"é") def test_unicode__decode_errors(self): @@ -172,7 +172,7 @@ class RendererTestCase(unittest.TestCase): """ renderer = Renderer() - renderer.default_encoding = "ascii" + renderer.string_encoding = "ascii" s = "déf" renderer.decode_errors = "ignore" @@ -289,12 +289,12 @@ class RendererTestCase(unittest.TestCase): renderer = Renderer() template = "déf" - # Check that decode_errors and default_encoding are both respected. + # Check that decode_errors and string_encoding are both respected. renderer.decode_errors = 'ignore' - renderer.default_encoding = 'ascii' + renderer.string_encoding = 'ascii' self.assertEquals(renderer.render(template), "df") - renderer.default_encoding = 'utf_8' + renderer.string_encoding = 'utf_8' self.assertEquals(renderer.render(template), u"déf") def test_make_load_partial(self): @@ -387,7 +387,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): pass renderer = Renderer() - renderer.default_encoding = 'ascii' + renderer.string_encoding = 'ascii' renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')} engine = renderer._make_render_engine() @@ -439,7 +439,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): """ renderer = Renderer() - renderer.default_encoding = 'ascii' + renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() literal = engine.literal @@ -452,7 +452,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): """ renderer = Renderer() - renderer.default_encoding = 'ascii' + renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() literal = engine.literal @@ -520,7 +520,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase): """ renderer = Renderer() - renderer.default_encoding = 'ascii' + renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() escape = engine.escape -- cgit v1.2.1 From 5d8eecfeec632c1caa6fb36522a2f0fb76ad2faf Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 30 Mar 2012 19:24:12 -0700 Subject: Added to the README a link to the PyPI page. --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c16bcb4..39f3840 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,8 @@ The `mustache(5)`_ man page provides a good introduction to Mustache's syntax. For a more complete (and more current) description of Mustache's behavior, see the official `Mustache spec`_. -Pystache is `semantically versioned`_. This version of Pystache passes all -tests in `version 1.0.3`_ of the spec. +Pystache is `semantically versioned`_ and can be found on PyPI_. This +version of Pystache passes all tests in `version 1.0.3`_ of the spec. Logo: `David Phillips`_ @@ -133,6 +133,7 @@ Author .. _Mustache spec: https://github.com/mustache/spec .. _mustache(5): http://mustache.github.com/mustache.5.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html +.. _PyPI: http://pypi.python.org/pypi/pystache .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org .. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From f2bf491f457c17b3a06b90043018c5599e5517f5 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 09:45:34 -0700 Subject: Renamed custom_template.py to template_spec.py. --- pystache/custom_template.py | 202 ---------------------- pystache/init.py | 4 +- pystache/template_spec.py | 202 ++++++++++++++++++++++ tests/test_custom_template.py | 382 ------------------------------------------ tests/test_template_spec.py | 382 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 586 insertions(+), 586 deletions(-) delete mode 100644 pystache/custom_template.py create mode 100644 pystache/template_spec.py delete mode 100644 tests/test_custom_template.py create mode 100644 tests/test_template_spec.py diff --git a/pystache/custom_template.py b/pystache/custom_template.py deleted file mode 100644 index 5f6a79b..0000000 --- a/pystache/custom_template.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding: utf-8 - -""" -This module supports specifying custom template information per view. - -""" - -import os.path - -from .context import Context -from .loader import Loader -from .locator import Locator -from .renderer import Renderer - - -# TODO: consider renaming this to something like Template or TemplateInfo. -class CustomizedTemplate(object): - - """ - A mixin for specifying custom template information. - - Subclass this class only if template customizations are needed. - - The following attributes allow one to customize/override template - information on a per View basis. A None value means to use default - behavior and perform no customization. All attributes are initially - set to None. - - Attributes: - - template: the template to use, as a unicode string. - - template_rel_path: the path to the template file, relative to the - directory containing the module defining the class. - - template_rel_directory: the directory containing the template file, relative - to the directory containing the module defining the class. - - template_extension: the template file extension. Defaults to "mustache". - Pass False for no extension (i.e. extensionless template files). - - """ - - template = None - # TODO: remove template_path. - template_path = None - template_rel_path = None - template_rel_directory = None - template_name = None - template_extension = None - template_encoding = None - - -# TODO: remove this class. -class View(CustomizedTemplate): - - _renderer = None - - locator = Locator() - - def __init__(self, context=None): - """ - Construct a View instance. - - """ - context = Context.create(self, context) - - self.context = context - - def _get_renderer(self): - if self._renderer is None: - # We delay setting self._renderer until now (instead of, say, - # setting it in the constructor) in case the user changes after - # instantiation some of the attributes on which the Renderer - # depends. This lets users set the template_extension attribute, - # etc. after View.__init__() has already been called. - renderer = Renderer(file_encoding=self.template_encoding, - search_dirs=self.template_path, - file_extension=self.template_extension) - self._renderer = renderer - - return self._renderer - - def get_template(self): - """ - Return the current template after setting it, if necessary. - - """ - if not self.template: - template_name = self._get_template_name() - renderer = self._get_renderer() - self.template = renderer.load_template(template_name) - - return self.template - - def _get_template_name(self): - """ - Return the name of the template to load. - - If the template_name attribute is not set, then this method constructs - the template name from the class name as follows, for example: - - TemplatePartial => template_partial - - Otherwise, this method returns the template_name. - - """ - if self.template_name: - return self.template_name - - return self.locator.make_template_name(self) - - def render(self): - """ - Return the view rendered using the current context. - - """ - template = self.get_template() - renderer = self._get_renderer() - return renderer.render(template, self.context) - - -# TODO: finalize this class's name. -# TODO: get this class fully working with test cases, and then refactor -# and replace the View class. -class CustomLoader(object): - - """ - Supports loading a custom-specified template. - - """ - - def __init__(self, search_dirs=None, loader=None): - if loader is None: - loader = Loader() - - if search_dirs is None: - search_dirs = [] - - self.loader = loader - self.search_dirs = search_dirs - - # TODO: make this private. - def get_relative_template_location(self, view): - """ - Return the relative template path as a (dir, file_name) pair. - - """ - if view.template_rel_path is not None: - return os.path.split(view.template_rel_path) - - template_dir = view.template_rel_directory - - # Otherwise, we don't know the directory. - - # TODO: share code with the loader attribute here. - locator = Locator(extension=self.loader.extension) - - template_name = (view.template_name if view.template_name is not None else - locator.make_template_name(view)) - - file_name = locator.make_file_name(template_name, view.template_extension) - - return (template_dir, file_name) - - # TODO: make this private. - def get_template_path(self, view): - """ - Return the path to the view's associated template. - - """ - dir_path, file_name = self.get_relative_template_location(view) - - # TODO: share code with the loader attribute here. - locator = Locator(extension=self.loader.extension) - - if dir_path is None: - # Then we need to search for the path. - path = locator.find_path_by_object(self.search_dirs, view, file_name=file_name) - else: - obj_dir = locator.get_object_directory(view) - path = os.path.join(obj_dir, dir_path, file_name) - - return path - - def load(self, custom): - """ - Find and return the template associated to a CustomizedTemplate instance. - - Returns the template as a unicode string. - - Arguments: - - custom: a CustomizedTemplate instance. - - """ - if custom.template is not None: - return self.loader.unicode(custom.template, custom.template_encoding) - - path = self.get_template_path(custom) - - return self.loader.read(path, custom.template_encoding) diff --git a/pystache/init.py b/pystache/init.py index 75c34a5..ab59440 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,8 +5,8 @@ This module contains the initialization logic called by __init__.py. """ -from .custom_template import View -from .custom_template import CustomizedTemplate +from .template_spec import View +from .template_spec import CustomizedTemplate from .renderer import Renderer diff --git a/pystache/template_spec.py b/pystache/template_spec.py new file mode 100644 index 0000000..5f6a79b --- /dev/null +++ b/pystache/template_spec.py @@ -0,0 +1,202 @@ +# coding: utf-8 + +""" +This module supports specifying custom template information per view. + +""" + +import os.path + +from .context import Context +from .loader import Loader +from .locator import Locator +from .renderer import Renderer + + +# TODO: consider renaming this to something like Template or TemplateInfo. +class CustomizedTemplate(object): + + """ + A mixin for specifying custom template information. + + Subclass this class only if template customizations are needed. + + The following attributes allow one to customize/override template + information on a per View basis. A None value means to use default + behavior and perform no customization. All attributes are initially + set to None. + + Attributes: + + template: the template to use, as a unicode string. + + template_rel_path: the path to the template file, relative to the + directory containing the module defining the class. + + template_rel_directory: the directory containing the template file, relative + to the directory containing the module defining the class. + + template_extension: the template file extension. Defaults to "mustache". + Pass False for no extension (i.e. extensionless template files). + + """ + + template = None + # TODO: remove template_path. + template_path = None + template_rel_path = None + template_rel_directory = None + template_name = None + template_extension = None + template_encoding = None + + +# TODO: remove this class. +class View(CustomizedTemplate): + + _renderer = None + + locator = Locator() + + def __init__(self, context=None): + """ + Construct a View instance. + + """ + context = Context.create(self, context) + + self.context = context + + def _get_renderer(self): + if self._renderer is None: + # We delay setting self._renderer until now (instead of, say, + # setting it in the constructor) in case the user changes after + # instantiation some of the attributes on which the Renderer + # depends. This lets users set the template_extension attribute, + # etc. after View.__init__() has already been called. + renderer = Renderer(file_encoding=self.template_encoding, + search_dirs=self.template_path, + file_extension=self.template_extension) + self._renderer = renderer + + return self._renderer + + def get_template(self): + """ + Return the current template after setting it, if necessary. + + """ + if not self.template: + template_name = self._get_template_name() + renderer = self._get_renderer() + self.template = renderer.load_template(template_name) + + return self.template + + def _get_template_name(self): + """ + Return the name of the template to load. + + If the template_name attribute is not set, then this method constructs + the template name from the class name as follows, for example: + + TemplatePartial => template_partial + + Otherwise, this method returns the template_name. + + """ + if self.template_name: + return self.template_name + + return self.locator.make_template_name(self) + + def render(self): + """ + Return the view rendered using the current context. + + """ + template = self.get_template() + renderer = self._get_renderer() + return renderer.render(template, self.context) + + +# TODO: finalize this class's name. +# TODO: get this class fully working with test cases, and then refactor +# and replace the View class. +class CustomLoader(object): + + """ + Supports loading a custom-specified template. + + """ + + def __init__(self, search_dirs=None, loader=None): + if loader is None: + loader = Loader() + + if search_dirs is None: + search_dirs = [] + + self.loader = loader + self.search_dirs = search_dirs + + # TODO: make this private. + def get_relative_template_location(self, view): + """ + Return the relative template path as a (dir, file_name) pair. + + """ + if view.template_rel_path is not None: + return os.path.split(view.template_rel_path) + + template_dir = view.template_rel_directory + + # Otherwise, we don't know the directory. + + # TODO: share code with the loader attribute here. + locator = Locator(extension=self.loader.extension) + + template_name = (view.template_name if view.template_name is not None else + locator.make_template_name(view)) + + file_name = locator.make_file_name(template_name, view.template_extension) + + return (template_dir, file_name) + + # TODO: make this private. + def get_template_path(self, view): + """ + Return the path to the view's associated template. + + """ + dir_path, file_name = self.get_relative_template_location(view) + + # TODO: share code with the loader attribute here. + locator = Locator(extension=self.loader.extension) + + if dir_path is None: + # Then we need to search for the path. + path = locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + else: + obj_dir = locator.get_object_directory(view) + path = os.path.join(obj_dir, dir_path, file_name) + + return path + + def load(self, custom): + """ + Find and return the template associated to a CustomizedTemplate instance. + + Returns the template as a unicode string. + + Arguments: + + custom: a CustomizedTemplate instance. + + """ + if custom.template is not None: + return self.loader.unicode(custom.template, custom.template_encoding) + + path = self.get_template_path(custom) + + return self.loader.read(path, custom.template_encoding) diff --git a/tests/test_custom_template.py b/tests/test_custom_template.py deleted file mode 100644 index eedf431..0000000 --- a/tests/test_custom_template.py +++ /dev/null @@ -1,382 +0,0 @@ -# coding: utf-8 - -""" -Unit tests of view.py. - -""" - -import os.path -import sys -import unittest - -from examples.simple import Simple -from examples.complex_view import ComplexView -from examples.lambdas import Lambdas -from examples.inverted import Inverted, InvertedLists -from pystache import CustomizedTemplate as Template -from pystache import Renderer -from pystache import View -from pystache.custom_template import CustomLoader -from pystache.locator import Locator -from pystache.loader import Loader -from .common import AssertIsMixin -from .common import AssertStringMixin -from .common import DATA_DIR -from .data.views import SampleView -from .data.views import NonAscii - - -class Thing(object): - pass - - -class ViewTestCase(unittest.TestCase): - - def test_init(self): - """ - Test the constructor. - - """ - class TestView(View): - template = "foo" - - view = TestView() - self.assertEquals(view.template, "foo") - - def test_template_path(self): - """ - Test that View.template_path is respected. - - """ - class Tagless(View): - pass - - view = Tagless() - self.assertRaises(IOError, view.render) - - view = Tagless() - view.template_path = "examples" - self.assertEquals(view.render(), "No tags...") - - def test_template_path_for_partials(self): - """ - Test that View.template_rel_path is respected for partials. - - """ - class TestView(View): - template = "Partial: {{>tagless}}" - - view = TestView() - self.assertRaises(IOError, view.render) - - view = TestView() - view.template_path = "examples" - self.assertEquals(view.render(), "Partial: No tags...") - - def test_basic_method_calls(self): - view = Simple() - self.assertEquals(view.render(), "Hi pizza!") - - def test_non_callable_attributes(self): - view = Simple() - view.thing = 'Chris' - self.assertEquals(view.render(), "Hi Chris!") - - def test_complex(self): - renderer = Renderer() - expected = renderer.render(ComplexView()) - self.assertEquals(expected, """\ -

Colors

-""") - - def test_higher_order_replace(self): - view = Lambdas() - self.assertEquals(view.render(), - 'bar != bar. oh, it does!') - - def test_higher_order_rot13(self): - view = Lambdas() - view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') - - def test_higher_order_lambda(self): - view = Lambdas() - view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' - self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') - - def test_partials_with_lambda(self): - view = Lambdas() - view.template = '{{>partial_with_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') - - def test_hierarchical_partials_with_lambdas(self): - view = Lambdas() - view.template = '{{>partial_with_partial_and_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') - - def test_inverted(self): - view = Inverted() - self.assertEquals(view.render(), """one, two, three, empty list""") - - def test_accessing_properties_on_parent_object_from_child_objects(self): - parent = Thing() - parent.this = 'derp' - parent.children = [Thing()] - view = Simple(context={'parent': parent}) - view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" - - self.assertEquals(view.render(), 'derp') - - def test_inverted_lists(self): - view = InvertedLists() - self.assertEquals(view.render(), """one, two, three, empty list""") - - -class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): - - """ - Tests custom_template.CustomLoader. - - """ - - def test_init__defaults(self): - custom = CustomLoader() - - # Check the loader attribute. - loader = custom.loader - self.assertEquals(loader.extension, 'mustache') - self.assertEquals(loader.file_encoding, sys.getdefaultencoding()) - to_unicode = loader.to_unicode - - # Check search_dirs. - self.assertEquals(custom.search_dirs, []) - - def test_init__search_dirs(self): - search_dirs = ['a', 'b'] - loader = CustomLoader(search_dirs) - - self.assertEquals(loader.search_dirs, ['a', 'b']) - - def test_init__loader(self): - loader = Loader() - custom = CustomLoader([], loader=loader) - - self.assertIs(custom.loader, loader) - - # TODO: rename to something like _assert_load(). - def _assert_template(self, loader, custom, expected): - self.assertString(loader.load(custom), expected) - - def test_load__template__type_str(self): - """ - Test the template attribute: str string. - - """ - custom = Template() - custom.template = "abc" - - self._assert_template(CustomLoader(), custom, u"abc") - - def test_load__template__type_unicode(self): - """ - Test the template attribute: unicode string. - - """ - custom = Template() - custom.template = u"abc" - - self._assert_template(CustomLoader(), custom, u"abc") - - def test_load__template__unicode_non_ascii(self): - """ - Test the template attribute: non-ascii unicode string. - - """ - custom = Template() - custom.template = u"é" - - self._assert_template(CustomLoader(), custom, u"é") - - def test_load__template__with_template_encoding(self): - """ - Test the template attribute: with template encoding attribute. - - """ - custom = Template() - custom.template = u'é'.encode('utf-8') - - self.assertRaises(UnicodeDecodeError, self._assert_template, CustomLoader(), custom, u'é') - - custom.template_encoding = 'utf-8' - self._assert_template(CustomLoader(), custom, u'é') - - # TODO: make this test complete. - def test_load__template__correct_loader(self): - """ - Test that reader.unicode() is called correctly. - - This test tests that the correct reader is called with the correct - arguments. This is a catch-all test to supplement the other - test cases. It tests CustomLoader.load() independent of reader.unicode() - being implemented correctly (and tested). - - """ - class MockLoader(Loader): - - def __init__(self): - self.s = None - self.encoding = None - - # Overrides the existing method. - def unicode(self, s, encoding=None): - self.s = s - self.encoding = encoding - return u"foo" - - loader = MockLoader() - custom_loader = CustomLoader() - custom_loader.loader = loader - - view = Template() - view.template = "template-foo" - view.template_encoding = "encoding-foo" - - # Check that our unicode() above was called. - self._assert_template(custom_loader, view, u'foo') - self.assertEquals(loader.s, "template-foo") - self.assertEquals(loader.encoding, "encoding-foo") - - -# TODO: migrate these tests into the CustomLoaderTests class. -# TODO: rename the get_template() tests to test load(). -# TODO: condense, reorganize, and rename the tests so that it is -# clear whether we have full test coverage (e.g. organized by -# CustomizedTemplate attributes or something). -class CustomizedTemplateTests(unittest.TestCase): - - # TODO: rename this method to _make_loader(). - def _make_locator(self): - locator = CustomLoader(search_dirs=[DATA_DIR]) - return locator - - def _assert_template_location(self, view, expected): - locator = self._make_locator() - actual = locator.get_relative_template_location(view) - self.assertEquals(actual, expected) - - def test_get_relative_template_location(self): - """ - Test get_relative_template_location(): default behavior (no attributes set). - - """ - view = SampleView() - self._assert_template_location(view, (None, 'sample_view.mustache')) - - def test_get_relative_template_location__template_rel_path__file_name_only(self): - """ - Test get_relative_template_location(): template_rel_path attribute. - - """ - view = SampleView() - view.template_rel_path = 'template.txt' - self._assert_template_location(view, ('', 'template.txt')) - - def test_get_relative_template_location__template_rel_path__file_name_with_directory(self): - """ - Test get_relative_template_location(): template_rel_path attribute. - - """ - view = SampleView() - view.template_rel_path = 'foo/bar/template.txt' - self._assert_template_location(view, ('foo/bar', 'template.txt')) - - def test_get_relative_template_location__template_rel_directory(self): - """ - Test get_relative_template_location(): template_rel_directory attribute. - - """ - view = SampleView() - view.template_rel_directory = 'foo' - - self._assert_template_location(view, ('foo', 'sample_view.mustache')) - - def test_get_relative_template_location__template_name(self): - """ - Test get_relative_template_location(): template_name attribute. - - """ - view = SampleView() - view.template_name = 'new_name' - self._assert_template_location(view, (None, 'new_name.mustache')) - - def test_get_relative_template_location__template_extension(self): - """ - Test get_relative_template_location(): template_extension attribute. - - """ - view = SampleView() - view.template_extension = 'txt' - self._assert_template_location(view, (None, 'sample_view.txt')) - - def test_get_template_path__with_directory(self): - """ - Test get_template_path() with a view that has a directory specified. - - """ - locator = self._make_locator() - - view = SampleView() - view.template_rel_path = 'foo/bar.txt' - self.assertTrue(locator.get_relative_template_location(view)[0] is not None) - - actual = locator.get_template_path(view) - expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) - - self.assertEquals(actual, expected) - - def test_get_template_path__without_directory(self): - """ - Test get_template_path() with a view that doesn't have a directory specified. - - """ - locator = self._make_locator() - - view = SampleView() - self.assertTrue(locator.get_relative_template_location(view)[0] is None) - - actual = locator.get_template_path(view) - expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) - - self.assertEquals(actual, expected) - - def _assert_get_template(self, custom, expected): - locator = self._make_locator() - actual = locator.load(custom) - - self.assertEquals(type(actual), unicode) - self.assertEquals(actual, expected) - - def test_get_template(self): - """ - Test get_template(): default behavior (no attributes set). - - """ - view = SampleView() - - self._assert_get_template(view, u"ascii: abc") - - def test_get_template__template_encoding(self): - """ - Test get_template(): template_encoding attribute. - - """ - view = NonAscii() - - self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') - - view.template_encoding = 'utf-8' - self._assert_get_template(view, u"non-ascii: é") diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py new file mode 100644 index 0000000..7e208c6 --- /dev/null +++ b/tests/test_template_spec.py @@ -0,0 +1,382 @@ +# coding: utf-8 + +""" +Unit tests of view.py. + +""" + +import os.path +import sys +import unittest + +from examples.simple import Simple +from examples.complex_view import ComplexView +from examples.lambdas import Lambdas +from examples.inverted import Inverted, InvertedLists +from pystache import CustomizedTemplate as Template +from pystache import Renderer +from pystache import View +from pystache.template_spec import CustomLoader +from pystache.locator import Locator +from pystache.loader import Loader +from .common import AssertIsMixin +from .common import AssertStringMixin +from .common import DATA_DIR +from .data.views import SampleView +from .data.views import NonAscii + + +class Thing(object): + pass + + +class ViewTestCase(unittest.TestCase): + + def test_init(self): + """ + Test the constructor. + + """ + class TestView(View): + template = "foo" + + view = TestView() + self.assertEquals(view.template, "foo") + + def test_template_path(self): + """ + Test that View.template_path is respected. + + """ + class Tagless(View): + pass + + view = Tagless() + self.assertRaises(IOError, view.render) + + view = Tagless() + view.template_path = "examples" + self.assertEquals(view.render(), "No tags...") + + def test_template_path_for_partials(self): + """ + Test that View.template_rel_path is respected for partials. + + """ + class TestView(View): + template = "Partial: {{>tagless}}" + + view = TestView() + self.assertRaises(IOError, view.render) + + view = TestView() + view.template_path = "examples" + self.assertEquals(view.render(), "Partial: No tags...") + + def test_basic_method_calls(self): + view = Simple() + self.assertEquals(view.render(), "Hi pizza!") + + def test_non_callable_attributes(self): + view = Simple() + view.thing = 'Chris' + self.assertEquals(view.render(), "Hi Chris!") + + def test_complex(self): + renderer = Renderer() + expected = renderer.render(ComplexView()) + self.assertEquals(expected, """\ +

Colors

+""") + + def test_higher_order_replace(self): + view = Lambdas() + self.assertEquals(view.render(), + 'bar != bar. oh, it does!') + + def test_higher_order_rot13(self): + view = Lambdas() + view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' + self.assertEquals(view.render(), 'nopqrstuvwxyz') + + def test_higher_order_lambda(self): + view = Lambdas() + view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' + self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') + + def test_partials_with_lambda(self): + view = Lambdas() + view.template = '{{>partial_with_lambda}}' + self.assertEquals(view.render(), 'nopqrstuvwxyz') + + def test_hierarchical_partials_with_lambdas(self): + view = Lambdas() + view.template = '{{>partial_with_partial_and_lambda}}' + self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') + + def test_inverted(self): + view = Inverted() + self.assertEquals(view.render(), """one, two, three, empty list""") + + def test_accessing_properties_on_parent_object_from_child_objects(self): + parent = Thing() + parent.this = 'derp' + parent.children = [Thing()] + view = Simple(context={'parent': parent}) + view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" + + self.assertEquals(view.render(), 'derp') + + def test_inverted_lists(self): + view = InvertedLists() + self.assertEquals(view.render(), """one, two, three, empty list""") + + +class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): + + """ + Tests template_spec.CustomLoader. + + """ + + def test_init__defaults(self): + custom = CustomLoader() + + # Check the loader attribute. + loader = custom.loader + self.assertEquals(loader.extension, 'mustache') + self.assertEquals(loader.file_encoding, sys.getdefaultencoding()) + to_unicode = loader.to_unicode + + # Check search_dirs. + self.assertEquals(custom.search_dirs, []) + + def test_init__search_dirs(self): + search_dirs = ['a', 'b'] + loader = CustomLoader(search_dirs) + + self.assertEquals(loader.search_dirs, ['a', 'b']) + + def test_init__loader(self): + loader = Loader() + custom = CustomLoader([], loader=loader) + + self.assertIs(custom.loader, loader) + + # TODO: rename to something like _assert_load(). + def _assert_template(self, loader, custom, expected): + self.assertString(loader.load(custom), expected) + + def test_load__template__type_str(self): + """ + Test the template attribute: str string. + + """ + custom = Template() + custom.template = "abc" + + self._assert_template(CustomLoader(), custom, u"abc") + + def test_load__template__type_unicode(self): + """ + Test the template attribute: unicode string. + + """ + custom = Template() + custom.template = u"abc" + + self._assert_template(CustomLoader(), custom, u"abc") + + def test_load__template__unicode_non_ascii(self): + """ + Test the template attribute: non-ascii unicode string. + + """ + custom = Template() + custom.template = u"é" + + self._assert_template(CustomLoader(), custom, u"é") + + def test_load__template__with_template_encoding(self): + """ + Test the template attribute: with template encoding attribute. + + """ + custom = Template() + custom.template = u'é'.encode('utf-8') + + self.assertRaises(UnicodeDecodeError, self._assert_template, CustomLoader(), custom, u'é') + + custom.template_encoding = 'utf-8' + self._assert_template(CustomLoader(), custom, u'é') + + # TODO: make this test complete. + def test_load__template__correct_loader(self): + """ + Test that reader.unicode() is called correctly. + + This test tests that the correct reader is called with the correct + arguments. This is a catch-all test to supplement the other + test cases. It tests CustomLoader.load() independent of reader.unicode() + being implemented correctly (and tested). + + """ + class MockLoader(Loader): + + def __init__(self): + self.s = None + self.encoding = None + + # Overrides the existing method. + def unicode(self, s, encoding=None): + self.s = s + self.encoding = encoding + return u"foo" + + loader = MockLoader() + custom_loader = CustomLoader() + custom_loader.loader = loader + + view = Template() + view.template = "template-foo" + view.template_encoding = "encoding-foo" + + # Check that our unicode() above was called. + self._assert_template(custom_loader, view, u'foo') + self.assertEquals(loader.s, "template-foo") + self.assertEquals(loader.encoding, "encoding-foo") + + +# TODO: migrate these tests into the CustomLoaderTests class. +# TODO: rename the get_template() tests to test load(). +# TODO: condense, reorganize, and rename the tests so that it is +# clear whether we have full test coverage (e.g. organized by +# CustomizedTemplate attributes or something). +class CustomizedTemplateTests(unittest.TestCase): + + # TODO: rename this method to _make_loader(). + def _make_locator(self): + locator = CustomLoader(search_dirs=[DATA_DIR]) + return locator + + def _assert_template_location(self, view, expected): + locator = self._make_locator() + actual = locator.get_relative_template_location(view) + self.assertEquals(actual, expected) + + def test_get_relative_template_location(self): + """ + Test get_relative_template_location(): default behavior (no attributes set). + + """ + view = SampleView() + self._assert_template_location(view, (None, 'sample_view.mustache')) + + def test_get_relative_template_location__template_rel_path__file_name_only(self): + """ + Test get_relative_template_location(): template_rel_path attribute. + + """ + view = SampleView() + view.template_rel_path = 'template.txt' + self._assert_template_location(view, ('', 'template.txt')) + + def test_get_relative_template_location__template_rel_path__file_name_with_directory(self): + """ + Test get_relative_template_location(): template_rel_path attribute. + + """ + view = SampleView() + view.template_rel_path = 'foo/bar/template.txt' + self._assert_template_location(view, ('foo/bar', 'template.txt')) + + def test_get_relative_template_location__template_rel_directory(self): + """ + Test get_relative_template_location(): template_rel_directory attribute. + + """ + view = SampleView() + view.template_rel_directory = 'foo' + + self._assert_template_location(view, ('foo', 'sample_view.mustache')) + + def test_get_relative_template_location__template_name(self): + """ + Test get_relative_template_location(): template_name attribute. + + """ + view = SampleView() + view.template_name = 'new_name' + self._assert_template_location(view, (None, 'new_name.mustache')) + + def test_get_relative_template_location__template_extension(self): + """ + Test get_relative_template_location(): template_extension attribute. + + """ + view = SampleView() + view.template_extension = 'txt' + self._assert_template_location(view, (None, 'sample_view.txt')) + + def test_get_template_path__with_directory(self): + """ + Test get_template_path() with a view that has a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + view.template_rel_path = 'foo/bar.txt' + self.assertTrue(locator.get_relative_template_location(view)[0] is not None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) + + self.assertEquals(actual, expected) + + def test_get_template_path__without_directory(self): + """ + Test get_template_path() with a view that doesn't have a directory specified. + + """ + locator = self._make_locator() + + view = SampleView() + self.assertTrue(locator.get_relative_template_location(view)[0] is None) + + actual = locator.get_template_path(view) + expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) + + self.assertEquals(actual, expected) + + def _assert_get_template(self, custom, expected): + locator = self._make_locator() + actual = locator.load(custom) + + self.assertEquals(type(actual), unicode) + self.assertEquals(actual, expected) + + def test_get_template(self): + """ + Test get_template(): default behavior (no attributes set). + + """ + view = SampleView() + + self._assert_get_template(view, u"ascii: abc") + + def test_get_template__template_encoding(self): + """ + Test get_template(): template_encoding attribute. + + """ + view = NonAscii() + + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') + + view.template_encoding = 'utf-8' + self._assert_get_template(view, u"non-ascii: é") -- cgit v1.2.1 From ce5cc9748c8929a045d3993790bd01ee51fc7020 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 09:49:23 -0700 Subject: Renamed template.py to parsed.py. --- pystache/parsed.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ pystache/parser.py | 2 +- pystache/template.py | 45 --------------------------------------------- 3 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 pystache/parsed.py delete mode 100644 pystache/template.py diff --git a/pystache/parsed.py b/pystache/parsed.py new file mode 100644 index 0000000..12b5cb0 --- /dev/null +++ b/pystache/parsed.py @@ -0,0 +1,45 @@ +# coding: utf-8 + +""" +Exposes a class that represents a parsed (or compiled) template. + +This module is meant only for internal use. + +""" + + +class ParsedTemplate(object): + + def __init__(self, parse_tree): + """ + Arguments: + + parse_tree: a list, each element of which is either-- + + (1) a unicode string, or + (2) a "rendering" callable that accepts a Context instance + and returns a unicode string. + + The possible rendering callables are the return values of the + following functions: + + * RenderEngine._make_get_escaped() + * RenderEngine._make_get_inverse() + * RenderEngine._make_get_literal() + * RenderEngine._make_get_partial() + * RenderEngine._make_get_section() + + """ + self._parse_tree = parse_tree + + def render(self, context): + """ + Returns: a string of type unicode. + + """ + get_unicode = lambda val: val(context) if callable(val) else val + parts = map(get_unicode, self._parse_tree) + s = ''.join(parts) + + return unicode(s) + diff --git a/pystache/parser.py b/pystache/parser.py index f88f61b..d07ebf6 100644 --- a/pystache/parser.py +++ b/pystache/parser.py @@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module. import re -from template import ParsedTemplate +from parsed import ParsedTemplate DEFAULT_DELIMITERS = ('{{', '}}') diff --git a/pystache/template.py b/pystache/template.py deleted file mode 100644 index 12b5cb0..0000000 --- a/pystache/template.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 - -""" -Exposes a class that represents a parsed (or compiled) template. - -This module is meant only for internal use. - -""" - - -class ParsedTemplate(object): - - def __init__(self, parse_tree): - """ - Arguments: - - parse_tree: a list, each element of which is either-- - - (1) a unicode string, or - (2) a "rendering" callable that accepts a Context instance - and returns a unicode string. - - The possible rendering callables are the return values of the - following functions: - - * RenderEngine._make_get_escaped() - * RenderEngine._make_get_inverse() - * RenderEngine._make_get_literal() - * RenderEngine._make_get_partial() - * RenderEngine._make_get_section() - - """ - self._parse_tree = parse_tree - - def render(self, context): - """ - Returns: a string of type unicode. - - """ - get_unicode = lambda val: val(context) if callable(val) else val - parts = map(get_unicode, self._parse_tree) - s = ''.join(parts) - - return unicode(s) - -- cgit v1.2.1 From 1542217e351fd9c45d25178e0fc92a91a5449cee Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 09:53:29 -0700 Subject: Renamed CustomizedTemplate to TemplateSpec. --- pystache/init.py | 4 ++-- pystache/template_spec.py | 9 ++++----- tests/data/views.py | 6 +++--- tests/test_template_spec.py | 6 +++--- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pystache/init.py b/pystache/init.py index ab59440..5ca096a 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -6,11 +6,11 @@ This module contains the initialization logic called by __init__.py. """ from .template_spec import View -from .template_spec import CustomizedTemplate +from .template_spec import TemplateSpec from .renderer import Renderer -__all__ = ['render', 'Renderer', 'View', 'CustomizedTemplate'] +__all__ = ['render', 'Renderer', 'View', 'TemplateSpec'] def render(template, context=None, **kwargs): diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 5f6a79b..1fd54a9 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -13,8 +13,7 @@ from .locator import Locator from .renderer import Renderer -# TODO: consider renaming this to something like Template or TemplateInfo. -class CustomizedTemplate(object): +class TemplateSpec(object): """ A mixin for specifying custom template information. @@ -52,7 +51,7 @@ class CustomizedTemplate(object): # TODO: remove this class. -class View(CustomizedTemplate): +class View(TemplateSpec): _renderer = None @@ -185,13 +184,13 @@ class CustomLoader(object): def load(self, custom): """ - Find and return the template associated to a CustomizedTemplate instance. + Find and return the template associated to a TemplateSpec instance. Returns the template as a unicode string. Arguments: - custom: a CustomizedTemplate instance. + custom: a TemplateSpec instance. """ if custom.template is not None: diff --git a/tests/data/views.py b/tests/data/views.py index b96d968..4d9df02 100644 --- a/tests/data/views.py +++ b/tests/data/views.py @@ -1,6 +1,6 @@ # coding: utf-8 -from pystache import CustomizedTemplate +from pystache import TemplateSpec class SayHello(object): @@ -8,9 +8,9 @@ class SayHello(object): return "World" -class SampleView(CustomizedTemplate): +class SampleView(TemplateSpec): pass -class NonAscii(CustomizedTemplate): +class NonAscii(TemplateSpec): pass diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 7e208c6..7947de9 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -13,7 +13,7 @@ from examples.simple import Simple from examples.complex_view import ComplexView from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists -from pystache import CustomizedTemplate as Template +from pystache import TemplateSpec as Template from pystache import Renderer from pystache import View from pystache.template_spec import CustomLoader @@ -255,8 +255,8 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): # TODO: rename the get_template() tests to test load(). # TODO: condense, reorganize, and rename the tests so that it is # clear whether we have full test coverage (e.g. organized by -# CustomizedTemplate attributes or something). -class CustomizedTemplateTests(unittest.TestCase): +# TemplateSpec attributes or something). +class TemplateSpecTests(unittest.TestCase): # TODO: rename this method to _make_loader(). def _make_locator(self): -- cgit v1.2.1 From 4d8d41bb50286c90788a324b8e9bc3e1bee3a710 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 10:05:00 -0700 Subject: Renamed CustomLoader to SpecLoader. --- pystache/template_spec.py | 3 +-- tests/test_template_spec.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 1fd54a9..faab918 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -119,10 +119,9 @@ class View(TemplateSpec): return renderer.render(template, self.context) -# TODO: finalize this class's name. # TODO: get this class fully working with test cases, and then refactor # and replace the View class. -class CustomLoader(object): +class SpecLoader(object): """ Supports loading a custom-specified template. diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 7947de9..510e117 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -16,7 +16,7 @@ from examples.inverted import Inverted, InvertedLists from pystache import TemplateSpec as Template from pystache import Renderer from pystache import View -from pystache.template_spec import CustomLoader +from pystache.template_spec import SpecLoader from pystache.locator import Locator from pystache.loader import Loader from .common import AssertIsMixin @@ -136,15 +136,15 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), """one, two, three, empty list""") -class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): +class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): """ - Tests template_spec.CustomLoader. + Tests template_spec.SpecLoader. """ def test_init__defaults(self): - custom = CustomLoader() + custom = SpecLoader() # Check the loader attribute. loader = custom.loader @@ -157,13 +157,13 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): def test_init__search_dirs(self): search_dirs = ['a', 'b'] - loader = CustomLoader(search_dirs) + loader = SpecLoader(search_dirs) self.assertEquals(loader.search_dirs, ['a', 'b']) def test_init__loader(self): loader = Loader() - custom = CustomLoader([], loader=loader) + custom = SpecLoader([], loader=loader) self.assertIs(custom.loader, loader) @@ -179,7 +179,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = "abc" - self._assert_template(CustomLoader(), custom, u"abc") + self._assert_template(SpecLoader(), custom, u"abc") def test_load__template__type_unicode(self): """ @@ -189,7 +189,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u"abc" - self._assert_template(CustomLoader(), custom, u"abc") + self._assert_template(SpecLoader(), custom, u"abc") def test_load__template__unicode_non_ascii(self): """ @@ -199,7 +199,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u"é" - self._assert_template(CustomLoader(), custom, u"é") + self._assert_template(SpecLoader(), custom, u"é") def test_load__template__with_template_encoding(self): """ @@ -209,10 +209,10 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom = Template() custom.template = u'é'.encode('utf-8') - self.assertRaises(UnicodeDecodeError, self._assert_template, CustomLoader(), custom, u'é') + self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é') custom.template_encoding = 'utf-8' - self._assert_template(CustomLoader(), custom, u'é') + self._assert_template(SpecLoader(), custom, u'é') # TODO: make this test complete. def test_load__template__correct_loader(self): @@ -221,7 +221,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): This test tests that the correct reader is called with the correct arguments. This is a catch-all test to supplement the other - test cases. It tests CustomLoader.load() independent of reader.unicode() + test cases. It tests SpecLoader.load() independent of reader.unicode() being implemented correctly (and tested). """ @@ -238,7 +238,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): return u"foo" loader = MockLoader() - custom_loader = CustomLoader() + custom_loader = SpecLoader() custom_loader.loader = loader view = Template() @@ -251,7 +251,7 @@ class CustomLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): self.assertEquals(loader.encoding, "encoding-foo") -# TODO: migrate these tests into the CustomLoaderTests class. +# TODO: migrate these tests into the SpecLoaderTests class. # TODO: rename the get_template() tests to test load(). # TODO: condense, reorganize, and rename the tests so that it is # clear whether we have full test coverage (e.g. organized by @@ -260,7 +260,7 @@ class TemplateSpecTests(unittest.TestCase): # TODO: rename this method to _make_loader(). def _make_locator(self): - locator = CustomLoader(search_dirs=[DATA_DIR]) + locator = SpecLoader(search_dirs=[DATA_DIR]) return locator def _assert_template_location(self, view, expected): -- cgit v1.2.1 From 0acab11d58bfd17bcabc673dd44a2318559373f1 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 10:24:01 -0700 Subject: Updates to template_spec docstrings. --- pystache/template_spec.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index faab918..e31b71f 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module supports specifying custom template information per view. +This module supports customized (or special/specified) template loading. """ @@ -13,21 +13,24 @@ from .locator import Locator from .renderer import Renderer +# TODO: finish the class docstring. class TemplateSpec(object): """ - A mixin for specifying custom template information. + A mixin or interface for specifying custom template information. - Subclass this class only if template customizations are needed. + The "spec" in TemplateSpec can be taken to mean that the template + information is either "specified" or "special." - The following attributes allow one to customize/override template - information on a per View basis. A None value means to use default - behavior and perform no customization. All attributes are initially - set to None. + A view should subclass this class only if customized template loading + is needed. The following attributes allow one to customize/override + template information on a per view basis. A None value means to use + default behavior for that value and perform no customization. All + attributes are initialized to None. Attributes: - template: the template to use, as a unicode string. + template: the template as a string. template_rel_path: the path to the template file, relative to the directory containing the module defining the class. -- cgit v1.2.1 From f6b36707e2b0c5fed1a59f096f750bad70668272 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 10:29:40 -0700 Subject: Refactored loader.Loader to use a _make_locator() method. --- pystache/loader.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index d7cdca1..fe7be16 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -51,8 +51,8 @@ class Loader(object): It should accept a string of type str and an optional encoding name and return a string of type unicode. Defaults to calling - Python's built-in function unicode() using the package encoding - and decode-errors defaults. + Python's built-in function unicode() using the package string + encoding and decode errors defaults. """ if extension is None: @@ -68,6 +68,9 @@ class Loader(object): self.file_encoding = file_encoding self.to_unicode = to_unicode + def _make_locator(self): + return Locator(extension=self.extension) + def unicode(self, s, encoding=None): """ Convert a string to unicode using the given encoding, and return it. @@ -116,7 +119,7 @@ class Loader(object): search_dirs: the list of directories in which to search. """ - locator = Locator(extension=self.extension) + locator = self._make_locator() path = locator.find_path_by_name(search_dirs, name) @@ -134,7 +137,7 @@ class Loader(object): search_dirs: the list of directories in which to search. """ - locator = Locator(extension=self.extension) + locator = self._make_locator() path = locator.find_path_by_object(search_dirs, obj) -- cgit v1.2.1 From 8f8fd6c45821d2a46d161e47183edf8be343e445 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 31 Mar 2012 10:39:29 -0700 Subject: Renamed custom to spec inside SpecLoader.load(). --- pystache/template_spec.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index e31b71f..e657d42 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -184,7 +184,7 @@ class SpecLoader(object): return path - def load(self, custom): + def load(self, spec): """ Find and return the template associated to a TemplateSpec instance. @@ -192,12 +192,12 @@ class SpecLoader(object): Arguments: - custom: a TemplateSpec instance. + spec: a TemplateSpec instance. """ - if custom.template is not None: - return self.loader.unicode(custom.template, custom.template_encoding) + if spec.template is not None: + return self.loader.unicode(spec.template, spec.template_encoding) - path = self.get_template_path(custom) + path = self.get_template_path(spec) - return self.loader.read(path, custom.template_encoding) + return self.loader.read(path, spec.template_encoding) -- cgit v1.2.1 From ed896eb91003237c3fad562bfe6bf32ef7bb2414 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 09:58:53 -0700 Subject: Renamed Locator methods to find_name and find_object. --- pystache/loader.py | 6 ++++-- pystache/locator.py | 4 ++-- pystache/template_spec.py | 15 +++++++-------- tests/test_locator.py | 34 +++++++++++++++++----------------- tests/test_template_spec.py | 12 ++++++------ 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index fe7be16..e2a9029 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -107,6 +107,7 @@ class Loader(object): return self.unicode(text, encoding) + # TODO: consider passing search_dirs in the constructor. # TODO: unit-test this method. def load_name(self, name, search_dirs): """ @@ -121,10 +122,11 @@ class Loader(object): """ locator = self._make_locator() - path = locator.find_path_by_name(search_dirs, name) + path = locator.find_name(search_dirs, name) return self.read(path) + # TODO: consider passing search_dirs in the constructor. # TODO: unit-test this method. def load_object(self, obj, search_dirs): """ @@ -139,6 +141,6 @@ class Loader(object): """ locator = self._make_locator() - path = locator.find_path_by_object(search_dirs, obj) + path = locator.find_object(search_dirs, obj) return self.read(path) diff --git a/pystache/locator.py b/pystache/locator.py index 6bc7d64..05bb445 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -123,7 +123,7 @@ class Locator(object): return path - def find_path_by_name(self, search_dirs, template_name): + def find_name(self, search_dirs, template_name): """ Return the path to a template with the given name. @@ -132,7 +132,7 @@ class Locator(object): return self._find_path_required(search_dirs, file_name) - def find_path_by_object(self, search_dirs, obj, file_name=None): + def find_object(self, search_dirs, obj, file_name=None): """ Return the path to a template associated with the given object. diff --git a/pystache/template_spec.py b/pystache/template_spec.py index e657d42..adf618a 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module supports customized (or special/specified) template loading. +This module supports customized (aka special or specified) template loading. """ @@ -164,22 +164,21 @@ class SpecLoader(object): return (template_dir, file_name) - # TODO: make this private. - def get_template_path(self, view): + def _find(self, spec): """ - Return the path to the view's associated template. + Find and return the path to the template associated to the instance. """ - dir_path, file_name = self.get_relative_template_location(view) + dir_path, file_name = self.get_relative_template_location(spec) # TODO: share code with the loader attribute here. locator = Locator(extension=self.loader.extension) if dir_path is None: # Then we need to search for the path. - path = locator.find_path_by_object(self.search_dirs, view, file_name=file_name) + path = locator.find_object(self.search_dirs, spec, file_name=file_name) else: - obj_dir = locator.get_object_directory(view) + obj_dir = locator.get_object_directory(spec) path = os.path.join(obj_dir, dir_path, file_name) return path @@ -198,6 +197,6 @@ class SpecLoader(object): if spec.template is not None: return self.loader.unicode(spec.template, spec.template_encoding) - path = self.get_template_path(spec) + path = self._find(spec) return self.loader.read(path, spec.template_encoding) diff --git a/tests/test_locator.py b/tests/test_locator.py index 84dbf44..fc33a2f 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -69,21 +69,21 @@ class LocatorTests(unittest.TestCase): self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar') - def test_find_path_by_name(self): + def test_find_name(self): locator = Locator() - path = locator.find_path_by_name(search_dirs=['examples'], template_name='simple') + path = locator.find_name(search_dirs=['examples'], template_name='simple') self.assertEquals(os.path.basename(path), 'simple.mustache') - def test_find_path_by_name__using_list_of_paths(self): + def test_find_name__using_list_of_paths(self): locator = Locator() - path = locator.find_path_by_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple') + path = locator.find_name(search_dirs=['doesnt_exist', 'examples'], template_name='simple') self.assertTrue(path) - def test_find_path_by_name__precedence(self): + def test_find_name__precedence(self): """ - Test the order in which find_path_by_name() searches directories. + Test the order in which find_name() searches directories. """ locator = Locator() @@ -91,47 +91,47 @@ class LocatorTests(unittest.TestCase): dir1 = DATA_DIR dir2 = os.path.join(DATA_DIR, 'locator') - self.assertTrue(locator.find_path_by_name(search_dirs=[dir1], template_name='duplicate')) - self.assertTrue(locator.find_path_by_name(search_dirs=[dir2], template_name='duplicate')) + self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate')) + self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate')) - path = locator.find_path_by_name(search_dirs=[dir2, dir1], template_name='duplicate') + path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate') dirpath = os.path.dirname(path) dirname = os.path.split(dirpath)[-1] self.assertEquals(dirname, 'locator') - def test_find_path_by_name__non_existent_template_fails(self): + def test_find_name__non_existent_template_fails(self): locator = Locator() - self.assertRaises(IOError, locator.find_path_by_name, search_dirs=[], template_name='doesnt_exist') + self.assertRaises(IOError, locator.find_name, search_dirs=[], template_name='doesnt_exist') - def test_find_path_by_object(self): + def test_find_object(self): locator = Locator() obj = SayHello() - actual = locator.find_path_by_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') + actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) self.assertEquals(actual, expected) - def test_find_path_by_object__none_file_name(self): + def test_find_object__none_file_name(self): locator = Locator() obj = SayHello() - actual = locator.find_path_by_object(search_dirs=[], obj=obj) + actual = locator.find_object(search_dirs=[], obj=obj) expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache')) self.assertEquals(actual, expected) - def test_find_path_by_object__none_object_directory(self): + def test_find_object__none_object_directory(self): locator = Locator() obj = None self.assertEquals(None, locator.get_object_directory(obj)) - actual = locator.find_path_by_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') + actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') expected = os.path.join(DATA_DIR, 'say_hello.mustache') self.assertEquals(actual, expected) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 510e117..6c26d14 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -322,9 +322,9 @@ class TemplateSpecTests(unittest.TestCase): view.template_extension = 'txt' self._assert_template_location(view, (None, 'sample_view.txt')) - def test_get_template_path__with_directory(self): + def test_find__with_directory(self): """ - Test get_template_path() with a view that has a directory specified. + Test _find() with a view that has a directory specified. """ locator = self._make_locator() @@ -333,14 +333,14 @@ class TemplateSpecTests(unittest.TestCase): view.template_rel_path = 'foo/bar.txt' self.assertTrue(locator.get_relative_template_location(view)[0] is not None) - actual = locator.get_template_path(view) + actual = locator._find(view) expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) self.assertEquals(actual, expected) - def test_get_template_path__without_directory(self): + def test_find__without_directory(self): """ - Test get_template_path() with a view that doesn't have a directory specified. + Test _find() with a view that doesn't have a directory specified. """ locator = self._make_locator() @@ -348,7 +348,7 @@ class TemplateSpecTests(unittest.TestCase): view = SampleView() self.assertTrue(locator.get_relative_template_location(view)[0] is None) - actual = locator.get_template_path(view) + actual = locator._find(view) expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) self.assertEquals(actual, expected) -- cgit v1.2.1 From aabb1c6bb2137ab8ad8be9702f2bf9e0d75b7343 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 10:02:47 -0700 Subject: Rename SpecLoader method to _find_relative(). --- pystache/template_spec.py | 5 ++--- tests/test_template_spec.py | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index adf618a..d4f209c 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -141,8 +141,7 @@ class SpecLoader(object): self.loader = loader self.search_dirs = search_dirs - # TODO: make this private. - def get_relative_template_location(self, view): + def _find_relative(self, view): """ Return the relative template path as a (dir, file_name) pair. @@ -169,7 +168,7 @@ class SpecLoader(object): Find and return the path to the template associated to the instance. """ - dir_path, file_name = self.get_relative_template_location(spec) + dir_path, file_name = self._find_relative(spec) # TODO: share code with the loader attribute here. locator = Locator(extension=self.loader.extension) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 6c26d14..cbf61c4 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -265,38 +265,38 @@ class TemplateSpecTests(unittest.TestCase): def _assert_template_location(self, view, expected): locator = self._make_locator() - actual = locator.get_relative_template_location(view) + actual = locator._find_relative(view) self.assertEquals(actual, expected) - def test_get_relative_template_location(self): + def test_find_relative(self): """ - Test get_relative_template_location(): default behavior (no attributes set). + Test _find_relative(): default behavior (no attributes set). """ view = SampleView() self._assert_template_location(view, (None, 'sample_view.mustache')) - def test_get_relative_template_location__template_rel_path__file_name_only(self): + def test_find_relative__template_rel_path__file_name_only(self): """ - Test get_relative_template_location(): template_rel_path attribute. + Test _find_relative(): template_rel_path attribute. """ view = SampleView() view.template_rel_path = 'template.txt' self._assert_template_location(view, ('', 'template.txt')) - def test_get_relative_template_location__template_rel_path__file_name_with_directory(self): + def test_find_relative__template_rel_path__file_name_with_directory(self): """ - Test get_relative_template_location(): template_rel_path attribute. + Test _find_relative(): template_rel_path attribute. """ view = SampleView() view.template_rel_path = 'foo/bar/template.txt' self._assert_template_location(view, ('foo/bar', 'template.txt')) - def test_get_relative_template_location__template_rel_directory(self): + def test_find_relative__template_rel_directory(self): """ - Test get_relative_template_location(): template_rel_directory attribute. + Test _find_relative(): template_rel_directory attribute. """ view = SampleView() @@ -304,18 +304,18 @@ class TemplateSpecTests(unittest.TestCase): self._assert_template_location(view, ('foo', 'sample_view.mustache')) - def test_get_relative_template_location__template_name(self): + def test_find_relative__template_name(self): """ - Test get_relative_template_location(): template_name attribute. + Test _find_relative(): template_name attribute. """ view = SampleView() view.template_name = 'new_name' self._assert_template_location(view, (None, 'new_name.mustache')) - def test_get_relative_template_location__template_extension(self): + def test_find_relative__template_extension(self): """ - Test get_relative_template_location(): template_extension attribute. + Test _find_relative(): template_extension attribute. """ view = SampleView() @@ -331,7 +331,7 @@ class TemplateSpecTests(unittest.TestCase): view = SampleView() view.template_rel_path = 'foo/bar.txt' - self.assertTrue(locator.get_relative_template_location(view)[0] is not None) + self.assertTrue(locator._find_relative(view)[0] is not None) actual = locator._find(view) expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) @@ -346,7 +346,7 @@ class TemplateSpecTests(unittest.TestCase): locator = self._make_locator() view = SampleView() - self.assertTrue(locator.get_relative_template_location(view)[0] is None) + self.assertTrue(locator._find_relative(view)[0] is None) actual = locator._find(view) expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) -- cgit v1.2.1 From d26bdf330adfecd073bd5e9b1f9780ba9d2d30d6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 10:44:17 -0700 Subject: Minor tweaks to the SpecLoader class. --- pystache/template_spec.py | 32 ++++++++++++++++---------------- tests/test_template_spec.py | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index d4f209c..b554261 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -141,27 +141,28 @@ class SpecLoader(object): self.loader = loader self.search_dirs = search_dirs - def _find_relative(self, view): + def _find_relative(self, spec): """ - Return the relative template path as a (dir, file_name) pair. + Return the path to the template as a relative (dir, file_name) pair. - """ - if view.template_rel_path is not None: - return os.path.split(view.template_rel_path) - - template_dir = view.template_rel_directory + The directory returned is relative to the directory containing the + class definition of the given object. The method returns None for + this directory if the directory is unknown without first searching + the search directories. - # Otherwise, we don't know the directory. + """ + if spec.template_rel_path is not None: + return os.path.split(spec.template_rel_path) - # TODO: share code with the loader attribute here. - locator = Locator(extension=self.loader.extension) + # Otherwise, determine the file name separately. + locator = self.loader._make_locator() - template_name = (view.template_name if view.template_name is not None else - locator.make_template_name(view)) + template_name = (spec.template_name if spec.template_name is not None else + locator.make_template_name(spec)) - file_name = locator.make_file_name(template_name, view.template_extension) + file_name = locator.make_file_name(template_name, spec.template_extension) - return (template_dir, file_name) + return (spec.template_rel_directory, file_name) def _find(self, spec): """ @@ -170,8 +171,7 @@ class SpecLoader(object): """ dir_path, file_name = self._find_relative(spec) - # TODO: share code with the loader attribute here. - locator = Locator(extension=self.loader.extension) + locator = self.loader._make_locator() if dir_path is None: # Then we need to search for the path. diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index cbf61c4..13fd4e6 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -Unit tests of view.py. +Unit tests for template_spec.py. """ -- cgit v1.2.1 From 00fa136b0c2c5725148535fef58b481583ba7a5e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 11:30:58 -0700 Subject: Moved the search_dirs default into the defaults module. --- pystache/defaults.py | 5 +++++ pystache/renderer.py | 45 ++++++++++++++++++++------------------------- tests/test_renderer.py | 32 ++++++++++++++++---------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/pystache/defaults.py b/pystache/defaults.py index 69c4995..b696410 100644 --- a/pystache/defaults.py +++ b/pystache/defaults.py @@ -9,6 +9,7 @@ does not otherwise specify a value. """ import cgi +import os import sys @@ -30,6 +31,10 @@ STRING_ENCODING = sys.getdefaultencoding() # strings that arise from files. FILE_ENCODING = sys.getdefaultencoding() +# The starting list of directories in which to search for templates when +# loading a template by file name. +SEARCH_DIRS = [os.curdir] # i.e. ['.'] + # The escape function to apply to strings that require escaping when # rendering templates (e.g. for tags enclosed in double braces). # Only unicode strings will be passed to this function. diff --git a/pystache/renderer.py b/pystache/renderer.py index 4a7b11d..b67415e 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -5,8 +5,6 @@ This module provides a Renderer class to render templates. """ -import os - from . import defaults from .context import Context from .loader import Loader @@ -32,7 +30,6 @@ class Renderer(object): """ - # TODO: file_encoding should default to the package default. def __init__(self, file_encoding=None, string_encoding=None, decode_errors=None, search_dirs=None, file_extension=None, escape=None, partials=None): @@ -53,6 +50,10 @@ class Renderer(object): the file system -- using relevant instance attributes like search_dirs, file_encoding, etc. + decode_errors: the string to pass as the errors argument to the + built-in function unicode() when converting str strings to + unicode. Defaults to the package default. + escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode string (or subclass of unicode) and return an escaped string @@ -69,8 +70,16 @@ class Renderer(object): file_encoding: the name of the default encoding to use when reading template files. All templates are converted to unicode prior to parsing. This encoding is used when reading template files - and converting them to unicode. Defaults to the value of the - string_encoding argument. + and converting them to unicode. Defaults to the package default. + + file_extension: the template file extension. Pass False for no + extension (i.e. to use extensionless template files). + Defaults to the package default. + + search_dirs: the list of directories in which to search for + templates when loading a template by name. If given a string, + the method interprets the string as a single directory. + Defaults to the package default. string_encoding: the name of the encoding to use when converting to unicode any strings of type str encountered during the @@ -78,49 +87,35 @@ class Renderer(object): argument to the built-in function unicode(). Defaults to the package default. - decode_errors: the string to pass as the errors argument to the - built-in function unicode() when converting str strings to - unicode. Defaults to the package default. - - search_dirs: the list of directories in which to search for - templates when loading a template by name. Defaults to the - current working directory. If given a string, the string is - interpreted as a single directory. - - file_extension: the template file extension. Pass False for no - extension (i.e. to use extensionless template files). - Defaults to the package default. - """ if decode_errors is None: decode_errors = defaults.DECODE_ERRORS - if string_encoding is None: - string_encoding = defaults.STRING_ENCODING - if escape is None: escape = defaults.TAG_ESCAPE - # This needs to be after we set the default string_encoding. if file_encoding is None: - file_encoding = string_encoding + file_encoding = defaults.FILE_ENCODING if file_extension is None: file_extension = defaults.TEMPLATE_EXTENSION if search_dirs is None: - search_dirs = os.curdir # i.e. "." + search_dirs = defaults.SEARCH_DIRS + + if string_encoding is None: + string_encoding = defaults.STRING_ENCODING if isinstance(search_dirs, basestring): search_dirs = [search_dirs] self.decode_errors = decode_errors - self.string_encoding = string_encoding self.escape = escape self.file_encoding = file_encoding self.file_extension = file_extension self.partials = partials self.search_dirs = search_dirs + self.string_encoding = string_encoding def _to_unicode_soft(self, s): """ diff --git a/tests/test_renderer.py b/tests/test_renderer.py index c256384..52a847b 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -54,22 +54,6 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer(escape=escape) self.assertEquals(renderer.escape("bar"), "**bar") - def test_string_encoding__default(self): - """ - Check the default value. - - """ - renderer = Renderer() - self.assertEquals(renderer.string_encoding, sys.getdefaultencoding()) - - def test_string_encoding(self): - """ - Check that the constructor sets the attribute correctly. - - """ - renderer = Renderer(string_encoding="foo") - self.assertEquals(renderer.string_encoding, "foo") - def test_decode_errors__default(self): """ Check the default value. @@ -142,6 +126,22 @@ class RendererInitTestCase(unittest.TestCase): renderer = Renderer(search_dirs=['foo']) self.assertEquals(renderer.search_dirs, ['foo']) + def test_string_encoding__default(self): + """ + Check the default value. + + """ + renderer = Renderer() + self.assertEquals(renderer.string_encoding, sys.getdefaultencoding()) + + def test_string_encoding(self): + """ + Check that the constructor sets the attribute correctly. + + """ + renderer = Renderer(string_encoding="foo") + self.assertEquals(renderer.string_encoding, "foo") + class RendererTestCase(unittest.TestCase): -- cgit v1.2.1 From 423db900a6b3b447c15f292c14b6fb794af18061 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 11:38:59 -0700 Subject: The Renderer class now passes search_dirs to Loader via the constructor. --- pystache/loader.py | 23 ++++++++++++++++------- pystache/renderer.py | 10 +++++----- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index e2a9029..670fcb7 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -31,7 +31,8 @@ class Loader(object): """ - def __init__(self, file_encoding=None, extension=None, to_unicode=None): + def __init__(self, file_encoding=None, extension=None, to_unicode=None, + search_dirs=None): """ Construct a template loader instance. @@ -44,6 +45,9 @@ class Loader(object): file_encoding: the name of the encoding to use when converting file contents to unicode. Defaults to the package default. + search_dirs: the list of directories in which to search when loading + a template by name or file name. Defaults to the package default. + to_unicode: the function to use when converting strings of type str to unicode. The function should have the signature: @@ -61,11 +65,16 @@ class Loader(object): if file_encoding is None: file_encoding = defaults.FILE_ENCODING + if search_dirs is None: + search_dirs = defaults.SEARCH_DIRS + if to_unicode is None: to_unicode = _to_unicode self.extension = extension self.file_encoding = file_encoding + # TODO: unit test setting this attribute. + self.search_dirs = search_dirs self.to_unicode = to_unicode def _make_locator(self): @@ -107,9 +116,8 @@ class Loader(object): return self.unicode(text, encoding) - # TODO: consider passing search_dirs in the constructor. # TODO: unit-test this method. - def load_name(self, name, search_dirs): + def load_name(self, name): """ Find and return the template with the given name. @@ -122,13 +130,13 @@ class Loader(object): """ locator = self._make_locator() - path = locator.find_name(search_dirs, name) + # TODO: change the order of these arguments. + path = locator.find_name(self.search_dirs, name) return self.read(path) - # TODO: consider passing search_dirs in the constructor. # TODO: unit-test this method. - def load_object(self, obj, search_dirs): + def load_object(self, obj): """ Find and return the template associated to the given object. @@ -141,6 +149,7 @@ class Loader(object): """ locator = self._make_locator() - path = locator.find_object(search_dirs, obj) + # TODO: change the order of these arguments. + path = locator.find_object(self.search_dirs, obj) return self.read(path) diff --git a/pystache/renderer.py b/pystache/renderer.py index b67415e..7b72803 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -76,8 +76,8 @@ class Renderer(object): extension (i.e. to use extensionless template files). Defaults to the package default. - search_dirs: the list of directories in which to search for - templates when loading a template by name. If given a string, + search_dirs: the list of directories in which to search when + loading a template by name or file name. If given a string, the method interprets the string as a single directory. Defaults to the package default. @@ -167,7 +167,7 @@ class Renderer(object): """ return Loader(file_encoding=self.file_encoding, extension=self.file_extension, - to_unicode=self.unicode) + to_unicode=self.unicode, search_dirs=self.search_dirs) def _make_load_template(self): """ @@ -177,7 +177,7 @@ class Renderer(object): loader = self._make_loader() def load_template(template_name): - return loader.load_name(template_name, self.search_dirs) + return loader.load_name(template_name) return load_template @@ -250,7 +250,7 @@ class Renderer(object): """ loader = self._make_loader() - template = loader.load_object(obj, self.search_dirs) + template = loader.load_object(obj) context = [obj] + list(context) -- cgit v1.2.1 From e837155c158389d91f09adfb3af0413099e8dfd4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 11:42:32 -0700 Subject: Switched the argument order of Locator.find_name() and Locator.find_object(). --- pystache/loader.py | 4 ++-- pystache/locator.py | 4 ++-- pystache/template_spec.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 670fcb7..760789e 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -131,7 +131,7 @@ class Loader(object): locator = self._make_locator() # TODO: change the order of these arguments. - path = locator.find_name(self.search_dirs, name) + path = locator.find_name(name, self.search_dirs) return self.read(path) @@ -150,6 +150,6 @@ class Loader(object): locator = self._make_locator() # TODO: change the order of these arguments. - path = locator.find_object(self.search_dirs, obj) + path = locator.find_object(obj, self.search_dirs) return self.read(path) diff --git a/pystache/locator.py b/pystache/locator.py index 05bb445..e288049 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -123,7 +123,7 @@ class Locator(object): return path - def find_name(self, search_dirs, template_name): + def find_name(self, template_name, search_dirs): """ Return the path to a template with the given name. @@ -132,7 +132,7 @@ class Locator(object): return self._find_path_required(search_dirs, file_name) - def find_object(self, search_dirs, obj, file_name=None): + def find_object(self, obj, search_dirs, file_name=None): """ Return the path to a template associated with the given object. diff --git a/pystache/template_spec.py b/pystache/template_spec.py index b554261..83aebfd 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -175,7 +175,7 @@ class SpecLoader(object): if dir_path is None: # Then we need to search for the path. - path = locator.find_object(self.search_dirs, spec, file_name=file_name) + path = locator.find_object(spec, self.search_dirs, file_name=file_name) else: obj_dir = locator.get_object_directory(spec) path = os.path.join(obj_dir, dir_path, file_name) -- cgit v1.2.1 From 81c6a9c44e58c6ea5e4cea671be09cdd145119ee Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 11:50:52 -0700 Subject: Removed two completed TODO's. --- pystache/loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 760789e..cc15d23 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -130,7 +130,6 @@ class Loader(object): """ locator = self._make_locator() - # TODO: change the order of these arguments. path = locator.find_name(name, self.search_dirs) return self.read(path) @@ -149,7 +148,6 @@ class Loader(object): """ locator = self._make_locator() - # TODO: change the order of these arguments. path = locator.find_object(obj, self.search_dirs) return self.read(path) -- cgit v1.2.1 From 51e31f57d870ae0d0b7892c9ecebb9626fbb1b3c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 11:55:16 -0700 Subject: Removed search_dirs from the SpecLoader constructor. --- pystache/template_spec.py | 12 ++++-------- tests/test_template_spec.py | 15 +++------------ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 83aebfd..ff4979b 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -122,8 +122,8 @@ class View(TemplateSpec): return renderer.render(template, self.context) -# TODO: get this class fully working with test cases, and then refactor -# and replace the View class. +# TODO: add test cases for this class, and then refactor while replacing the +# View class. class SpecLoader(object): """ @@ -131,15 +131,11 @@ class SpecLoader(object): """ - def __init__(self, search_dirs=None, loader=None): + def __init__(self, loader=None): if loader is None: loader = Loader() - if search_dirs is None: - search_dirs = [] - self.loader = loader - self.search_dirs = search_dirs def _find_relative(self, spec): """ @@ -175,7 +171,7 @@ class SpecLoader(object): if dir_path is None: # Then we need to search for the path. - path = locator.find_object(spec, self.search_dirs, file_name=file_name) + path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) else: obj_dir = locator.get_object_directory(spec) path = os.path.join(obj_dir, dir_path, file_name) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 13fd4e6..b3d7093 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -150,20 +150,12 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): loader = custom.loader self.assertEquals(loader.extension, 'mustache') self.assertEquals(loader.file_encoding, sys.getdefaultencoding()) + # TODO: finish testing the other Loader attributes. to_unicode = loader.to_unicode - # Check search_dirs. - self.assertEquals(custom.search_dirs, []) - - def test_init__search_dirs(self): - search_dirs = ['a', 'b'] - loader = SpecLoader(search_dirs) - - self.assertEquals(loader.search_dirs, ['a', 'b']) - def test_init__loader(self): loader = Loader() - custom = SpecLoader([], loader=loader) + custom = SpecLoader(loader=loader) self.assertIs(custom.loader, loader) @@ -260,8 +252,7 @@ class TemplateSpecTests(unittest.TestCase): # TODO: rename this method to _make_loader(). def _make_locator(self): - locator = SpecLoader(search_dirs=[DATA_DIR]) - return locator + return SpecLoader() def _assert_template_location(self, view, expected): locator = self._make_locator() -- cgit v1.2.1 From 84b04ad3642a9c9c952dea069a3e91992dd6cb23 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 14:40:23 -0700 Subject: Moved deprecated View class into its own module. --- pystache/init.py | 4 +-- pystache/template_spec.py | 74 ++---------------------------------------- pystache/view.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 74 deletions(-) create mode 100644 pystache/view.py diff --git a/pystache/init.py b/pystache/init.py index 5ca096a..b629150 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,9 +5,9 @@ This module contains the initialization logic called by __init__.py. """ -from .template_spec import View -from .template_spec import TemplateSpec from .renderer import Renderer +from .template_spec import TemplateSpec +from .view import View __all__ = ['render', 'Renderer', 'View', 'TemplateSpec'] diff --git a/pystache/template_spec.py b/pystache/template_spec.py index ff4979b..68dc52d 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -7,12 +7,11 @@ This module supports customized (aka special or specified) template loading. import os.path -from .context import Context from .loader import Loader -from .locator import Locator -from .renderer import Renderer +# TODO: consider putting TemplateSpec and SpecLoader in separate modules. + # TODO: finish the class docstring. class TemplateSpec(object): @@ -53,75 +52,6 @@ class TemplateSpec(object): template_encoding = None -# TODO: remove this class. -class View(TemplateSpec): - - _renderer = None - - locator = Locator() - - def __init__(self, context=None): - """ - Construct a View instance. - - """ - context = Context.create(self, context) - - self.context = context - - def _get_renderer(self): - if self._renderer is None: - # We delay setting self._renderer until now (instead of, say, - # setting it in the constructor) in case the user changes after - # instantiation some of the attributes on which the Renderer - # depends. This lets users set the template_extension attribute, - # etc. after View.__init__() has already been called. - renderer = Renderer(file_encoding=self.template_encoding, - search_dirs=self.template_path, - file_extension=self.template_extension) - self._renderer = renderer - - return self._renderer - - def get_template(self): - """ - Return the current template after setting it, if necessary. - - """ - if not self.template: - template_name = self._get_template_name() - renderer = self._get_renderer() - self.template = renderer.load_template(template_name) - - return self.template - - def _get_template_name(self): - """ - Return the name of the template to load. - - If the template_name attribute is not set, then this method constructs - the template name from the class name as follows, for example: - - TemplatePartial => template_partial - - Otherwise, this method returns the template_name. - - """ - if self.template_name: - return self.template_name - - return self.locator.make_template_name(self) - - def render(self): - """ - Return the view rendered using the current context. - - """ - template = self.get_template() - renderer = self._get_renderer() - return renderer.render(template, self.context) - - # TODO: add test cases for this class, and then refactor while replacing the # View class. class SpecLoader(object): diff --git a/pystache/view.py b/pystache/view.py new file mode 100644 index 0000000..1aa31be --- /dev/null +++ b/pystache/view.py @@ -0,0 +1,82 @@ +# coding: utf-8 + +""" +This module exposes the deprecated View class. + +TODO: remove this module. + +""" + +from .context import Context +from .locator import Locator +from .renderer import Renderer +from .template_spec import TemplateSpec + + +# TODO: remove this class. +class View(TemplateSpec): + + _renderer = None + + locator = Locator() + + def __init__(self, context=None): + """ + Construct a View instance. + + """ + context = Context.create(self, context) + + self.context = context + + def _get_renderer(self): + if self._renderer is None: + # We delay setting self._renderer until now (instead of, say, + # setting it in the constructor) in case the user changes after + # instantiation some of the attributes on which the Renderer + # depends. This lets users set the template_extension attribute, + # etc. after View.__init__() has already been called. + renderer = Renderer(file_encoding=self.template_encoding, + search_dirs=self.template_path, + file_extension=self.template_extension) + self._renderer = renderer + + return self._renderer + + def get_template(self): + """ + Return the current template after setting it, if necessary. + + """ + if not self.template: + template_name = self._get_template_name() + renderer = self._get_renderer() + self.template = renderer.load_template(template_name) + + return self.template + + def _get_template_name(self): + """ + Return the name of the template to load. + + If the template_name attribute is not set, then this method constructs + the template name from the class name as follows, for example: + + TemplatePartial => template_partial + + Otherwise, this method returns the template_name. + + """ + if self.template_name: + return self.template_name + + return self.locator.make_template_name(self) + + def render(self): + """ + Return the view rendered using the current context. + + """ + template = self.get_template() + renderer = self._get_renderer() + return renderer.render(template, self.context) -- cgit v1.2.1 From a7e21be252218ff64498841fb60c9ef4c7bb66da Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 14:42:06 -0700 Subject: The Renderer class now creates a SpecLoader when necessary. --- pystache/renderer.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 7b72803..ca69dc5 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -9,6 +9,8 @@ from . import defaults from .context import Context from .loader import Loader from .renderengine import RenderEngine +from .template_spec import SpecLoader +from .template_spec import TemplateSpec class Renderer(object): @@ -250,7 +252,17 @@ class Renderer(object): """ loader = self._make_loader() - template = loader.load_object(obj) + + # TODO: consider an approach that does not require using an if + # block here. For example, perhaps this class's loader can be + # a SpecLoader in all cases, and the SpecLoader instance can + # check the object's type. Or perhaps Loader and SpecLoader + # can be refactored to implement the same interface. + if isinstance(obj, TemplateSpec): + loader = SpecLoader(loader) + template = loader.load(obj) + else: + template = loader.load_object(obj) context = [obj] + list(context) @@ -299,8 +311,8 @@ class Renderer(object): all items in the *context list. """ - if not isinstance(template, basestring): - # Then we assume the template is an object. - return self._render_object(template, *context, **kwargs) + if isinstance(template, basestring): + return self._render_string(template, *context, **kwargs) + # Otherwise, we assume the template is an object. - return self._render_string(template, *context, **kwargs) + return self._render_object(template, *context, **kwargs) -- cgit v1.2.1 From 9b6b76c4926ef812ee4892b4325ea8d3b98fb236 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 14:53:37 -0700 Subject: Added a unit test for rendering a TemplateSpec instance. --- tests/test_renderer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 52a847b..6737cb1 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -11,10 +11,12 @@ import sys import unittest from examples.simple import Simple -from pystache.renderer import Renderer +from pystache import Renderer +from pystache import TemplateSpec from pystache.loader import Loader from .common import get_data_path +from .common import AssertStringMixin from .data.views import SayHello @@ -143,7 +145,7 @@ class RendererInitTestCase(unittest.TestCase): self.assertEquals(renderer.string_encoding, "foo") -class RendererTestCase(unittest.TestCase): +class RendererTests(unittest.TestCase, AssertStringMixin): """Test the Renderer class.""" @@ -353,6 +355,21 @@ class RendererTestCase(unittest.TestCase): actual = renderer.render(say_hello, to='Mars') self.assertEquals('Hello, Mars', actual) + def test_render__template_spec(self): + """ + Test rendering a TemplateSpec instance. + + """ + renderer = Renderer() + + class Spec(TemplateSpec): + template = "hello, {{to}}" + to = 'world' + + spec = Spec() + actual = renderer.render(spec) + self.assertString(actual, u'hello, world') + def test_render__view(self): """ Test rendering a View instance. -- cgit v1.2.1 From dec5054d6f0f486af6b305c50f97ffc922e57019 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 15:15:18 -0700 Subject: Removed the View class from the inverted.py example. --- examples/inverted.py | 9 ++++----- tests/test_template_spec.py | 12 +++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/inverted.py b/examples/inverted.py index e0f7aaf..2a05302 100644 --- a/examples/inverted.py +++ b/examples/inverted.py @@ -1,7 +1,6 @@ -import pystache +from pystache import TemplateSpec -class Inverted(pystache.View): - template_path = 'examples' +class Inverted(object): def t(self): return True @@ -14,11 +13,11 @@ class Inverted(pystache.View): def empty_list(self): return [] - + def populated_list(self): return ['some_value'] -class InvertedLists(Inverted): +class InvertedLists(Inverted, TemplateSpec): template_name = 'inverted' def t(self): diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index b3d7093..5bb8210 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -30,7 +30,7 @@ class Thing(object): pass -class ViewTestCase(unittest.TestCase): +class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_init(self): """ @@ -119,8 +119,9 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') def test_inverted(self): - view = Inverted() - self.assertEquals(view.render(), """one, two, three, empty list""") + renderer = Renderer() + expected = renderer.render(Inverted()) + self.assertString(expected, u"""one, two, three, empty list""") def test_accessing_properties_on_parent_object_from_child_objects(self): parent = Thing() @@ -132,8 +133,9 @@ class ViewTestCase(unittest.TestCase): self.assertEquals(view.render(), 'derp') def test_inverted_lists(self): - view = InvertedLists() - self.assertEquals(view.render(), """one, two, three, empty list""") + renderer = Renderer() + expected = renderer.render(InvertedLists()) + self.assertString(expected, u"""one, two, three, empty list""") class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): -- cgit v1.2.1 From f5b37f729f81627dcf7ac94af5d2b2482b228c42 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 15:22:00 -0700 Subject: Removed the word "view" from the Complex example. --- examples/complex.mustache | 6 ++++++ examples/complex.py | 20 ++++++++++++++++++++ examples/complex_view.mustache | 6 ------ examples/complex_view.py | 20 -------------------- tests/test_simple.py | 4 ++-- tests/test_template_spec.py | 6 +++--- 6 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 examples/complex.mustache create mode 100644 examples/complex.py delete mode 100644 examples/complex_view.mustache delete mode 100644 examples/complex_view.py diff --git a/examples/complex.mustache b/examples/complex.mustache new file mode 100644 index 0000000..6de758b --- /dev/null +++ b/examples/complex.mustache @@ -0,0 +1,6 @@ +

{{ header }}

+{{#list}} +
    +{{#item}}{{# current }}
  • {{name}}
  • +{{/ current }}{{#link}}
  • {{name}}
  • +{{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}} \ No newline at end of file diff --git a/examples/complex.py b/examples/complex.py new file mode 100644 index 0000000..e3f1767 --- /dev/null +++ b/examples/complex.py @@ -0,0 +1,20 @@ +class Complex(object): + + def header(self): + return "Colors" + + def item(self): + items = [] + items.append({ 'name': 'red', 'current': True, 'url': '#Red' }) + items.append({ 'name': 'green', 'link': True, 'url': '#Green' }) + items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' }) + return items + + def list(self): + return not self.empty() + + def empty(self): + return len(self.item()) == 0 + + def empty_list(self): + return []; diff --git a/examples/complex_view.mustache b/examples/complex_view.mustache deleted file mode 100644 index 6de758b..0000000 --- a/examples/complex_view.mustache +++ /dev/null @@ -1,6 +0,0 @@ -

{{ header }}

-{{#list}} -
    -{{#item}}{{# current }}
  • {{name}}
  • -{{/ current }}{{#link}}
  • {{name}}
  • -{{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}} \ No newline at end of file diff --git a/examples/complex_view.py b/examples/complex_view.py deleted file mode 100644 index 6edbbb5..0000000 --- a/examples/complex_view.py +++ /dev/null @@ -1,20 +0,0 @@ -class ComplexView(object): - - def header(self): - return "Colors" - - def item(self): - items = [] - items.append({ 'name': 'red', 'current': True, 'url': '#Red' }) - items.append({ 'name': 'green', 'link': True, 'url': '#Green' }) - items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' }) - return items - - def list(self): - return not self.empty() - - def empty(self): - return len(self.item()) == 0 - - def empty_list(self): - return []; diff --git a/tests/test_simple.py b/tests/test_simple.py index 3b5f188..951730d 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -3,7 +3,7 @@ import unittest import pystache from pystache import Renderer from examples.nested_context import NestedContext -from examples.complex_view import ComplexView +from examples.complex import Complex from examples.lambdas import Lambdas from examples.template_partial import TemplatePartial from examples.simple import Simple @@ -20,7 +20,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin): def test_looping_and_negation_context(self): template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' - context = ComplexView() + context = Complex() renderer = Renderer() expected = renderer.render(template, context) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 5bb8210..bd97465 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -10,7 +10,7 @@ import sys import unittest from examples.simple import Simple -from examples.complex_view import ComplexView +from examples.complex import Complex from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache import TemplateSpec as Template @@ -84,8 +84,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_complex(self): renderer = Renderer() - expected = renderer.render(ComplexView()) - self.assertEquals(expected, """\ + expected = renderer.render(Complex()) + self.assertString(expected, u"""\

Colors

  • red
  • -- cgit v1.2.1 From 1996af1c0c53da1111bb002fd3951f2f427f5c48 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 15:52:33 -0700 Subject: Removed View from Lambdas example. --- examples/lambdas.py | 8 +++++--- tests/test_simple.py | 5 ++++- tests/test_template_spec.py | 30 +++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/lambdas.py b/examples/lambdas.py index 01a3697..653531d 100644 --- a/examples/lambdas.py +++ b/examples/lambdas.py @@ -1,4 +1,4 @@ -import pystache +from pystache import TemplateSpec def rot(s, n=13): r = "" @@ -17,8 +17,10 @@ def rot(s, n=13): def replace(subject, this='foo', with_this='bar'): return subject.replace(this, with_this) -class Lambdas(pystache.View): - template_path = 'examples' + +# This class subclasses TemplateSpec because at least one unit test +# sets the template attribute. +class Lambdas(TemplateSpec): def replace_foo_with_bar(self, text=None): return replace diff --git a/tests/test_simple.py b/tests/test_simple.py index 951730d..0631574 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -33,7 +33,10 @@ class TestSimple(unittest.TestCase, AssertStringMixin): def test_callables(self): view = Lambdas() view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}' - self.assertEquals(view.render(), 'bar != bar. oh, it does!') + + renderer = Renderer() + expected = renderer.render(view) + self.assertString(expected, u'bar != bar. oh, it does!') def test_rendering_partial(self): view = TemplatePartial() diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index bd97465..658061b 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -9,6 +9,7 @@ import os.path import sys import unittest +import examples from examples.simple import Simple from examples.complex import Complex from examples.lambdas import Lambdas @@ -26,6 +27,9 @@ from .data.views import SampleView from .data.views import NonAscii +EXAMPLES_DIR = os.path.dirname(examples.__file__) + + class Thing(object): pass @@ -94,29 +98,41 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
""") def test_higher_order_replace(self): - view = Lambdas() - self.assertEquals(view.render(), - 'bar != bar. oh, it does!') + renderer = Renderer() + expected = renderer.render(Lambdas()) + self.assertEquals(expected, 'bar != bar. oh, it does!') def test_higher_order_rot13(self): view = Lambdas() view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') + + renderer = Renderer() + expected = renderer.render(view) + self.assertString(expected, u'nopqrstuvwxyz') def test_higher_order_lambda(self): view = Lambdas() view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' - self.assertEquals(view.render(), 'abcdefghijklmnopqrstuvwxyz') + + renderer = Renderer() + expected = renderer.render(view) + self.assertString(expected, u'abcdefghijklmnopqrstuvwxyz') def test_partials_with_lambda(self): view = Lambdas() view.template = '{{>partial_with_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyz') + + renderer = Renderer(search_dirs=EXAMPLES_DIR) + expected = renderer.render(view) + self.assertEquals(expected, u'nopqrstuvwxyz') def test_hierarchical_partials_with_lambdas(self): view = Lambdas() view.template = '{{>partial_with_partial_and_lambda}}' - self.assertEquals(view.render(), 'nopqrstuvwxyznopqrstuvwxyz') + + renderer = Renderer(search_dirs=EXAMPLES_DIR) + expected = renderer.render(view) + self.assertString(expected, u'nopqrstuvwxyznopqrstuvwxyz') def test_inverted(self): renderer = Renderer() -- cgit v1.2.1 From de8aed5300026cf4865509ac08298cf0fc5ca471 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 16:31:00 -0700 Subject: Removed View from NestedContext example and added experimental Renderer.context property. Also corrected some test variable names (expected -> actual). :) --- examples/nested_context.py | 13 +++++++++---- pystache/renderer.py | 15 +++++++++++++++ tests/test_examples.py | 12 +++++++++--- tests/test_simple.py | 7 +++++-- tests/test_template_spec.py | 32 ++++++++++++++++---------------- 5 files changed, 54 insertions(+), 25 deletions(-) diff --git a/examples/nested_context.py b/examples/nested_context.py index 6fd0aea..4626ac0 100644 --- a/examples/nested_context.py +++ b/examples/nested_context.py @@ -1,7 +1,12 @@ -import pystache +from pystache import TemplateSpec -class NestedContext(pystache.View): - template_path = 'examples' +class NestedContext(TemplateSpec): + + def __init__(self, renderer): + self.renderer = renderer + + def _context_get(self, key): + return self.renderer.context.get(key) def outer_thing(self): return "two" @@ -16,6 +21,6 @@ class NestedContext(pystache.View): return [{'outer': 'car'}] def nested_context_in_view(self): - if self.context.get('outer') == self.context.get('inner'): + if self._context_get('outer') == self._context_get('inner'): return 'it works!' return '' diff --git a/pystache/renderer.py b/pystache/renderer.py index ca69dc5..a02693c 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -111,6 +111,7 @@ class Renderer(object): if isinstance(search_dirs, basestring): search_dirs = [search_dirs] + self._context = None self.decode_errors = decode_errors self.escape = escape self.file_encoding = file_encoding @@ -119,6 +120,19 @@ class Renderer(object): self.search_dirs = search_dirs self.string_encoding = string_encoding + # This is an experimental way of giving views access to the current context. + # TODO: consider another approach of not giving access via a property, + # but instead letting the caller pass the initial context to the + # main render() method by reference. This approach would probably + # be less likely to be misused. + @property + def context(self): + """ + Return the current rendering context [experimental]. + + """ + return self._context + def _to_unicode_soft(self, s): """ Convert a basestring to unicode, preserving any unicode subclass. @@ -240,6 +254,7 @@ class Renderer(object): template = self._to_unicode_hard(template) context = Context.create(*context, **kwargs) + self._context = context engine = self._make_render_engine() rendered = engine.render(template, context) diff --git a/tests/test_examples.py b/tests/test_examples.py index e4b41bf..b7df909 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -63,12 +63,18 @@ Again, Welcome!""") """) def test_nested_context(self): - self.assertEquals(NestedContext().render(), "one and foo and two") + renderer = Renderer() + actual = renderer.render(NestedContext(renderer)) + self.assertString(actual, u"one and foo and two") def test_nested_context_is_available_in_view(self): - view = NestedContext() + renderer = Renderer() + + view = NestedContext(renderer) view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}' - self.assertEquals(view.render(), 'it works!') + + actual = renderer.render(view) + self.assertString(actual, u'it works!') def test_partial_in_partial_has_access_to_grand_parent_context(self): view = TemplatePartial(context = {'prop': 'derp'}) diff --git a/tests/test_simple.py b/tests/test_simple.py index 0631574..aba0e89 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -14,9 +14,12 @@ from tests.common import AssertStringMixin class TestSimple(unittest.TestCase, AssertStringMixin): def test_nested_context(self): - view = NestedContext() + renderer = Renderer() + view = NestedContext(renderer) view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}' - self.assertEquals(view.render(), "one and foo and two") + + actual = renderer.render(view) + self.assertString(actual, u"one and foo and two") def test_looping_and_negation_context(self): template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 658061b..692f93e 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -88,8 +88,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_complex(self): renderer = Renderer() - expected = renderer.render(Complex()) - self.assertString(expected, u"""\ + actual = renderer.render(Complex()) + self.assertString(actual, u"""\

Colors

  • red
  • @@ -99,45 +99,45 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_higher_order_replace(self): renderer = Renderer() - expected = renderer.render(Lambdas()) - self.assertEquals(expected, 'bar != bar. oh, it does!') + actual = renderer.render(Lambdas()) + self.assertEquals(actual, 'bar != bar. oh, it does!') def test_higher_order_rot13(self): view = Lambdas() view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' renderer = Renderer() - expected = renderer.render(view) - self.assertString(expected, u'nopqrstuvwxyz') + actual = renderer.render(view) + self.assertString(actual, u'nopqrstuvwxyz') def test_higher_order_lambda(self): view = Lambdas() view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' renderer = Renderer() - expected = renderer.render(view) - self.assertString(expected, u'abcdefghijklmnopqrstuvwxyz') + actual = renderer.render(view) + self.assertString(actual, u'abcdefghijklmnopqrstuvwxyz') def test_partials_with_lambda(self): view = Lambdas() view.template = '{{>partial_with_lambda}}' renderer = Renderer(search_dirs=EXAMPLES_DIR) - expected = renderer.render(view) - self.assertEquals(expected, u'nopqrstuvwxyz') + actual = renderer.render(view) + self.assertEquals(actual, u'nopqrstuvwxyz') def test_hierarchical_partials_with_lambdas(self): view = Lambdas() view.template = '{{>partial_with_partial_and_lambda}}' renderer = Renderer(search_dirs=EXAMPLES_DIR) - expected = renderer.render(view) - self.assertString(expected, u'nopqrstuvwxyznopqrstuvwxyz') + actual = renderer.render(view) + self.assertString(actual, u'nopqrstuvwxyznopqrstuvwxyz') def test_inverted(self): renderer = Renderer() - expected = renderer.render(Inverted()) - self.assertString(expected, u"""one, two, three, empty list""") + actual = renderer.render(Inverted()) + self.assertString(actual, u"""one, two, three, empty list""") def test_accessing_properties_on_parent_object_from_child_objects(self): parent = Thing() @@ -150,8 +150,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_inverted_lists(self): renderer = Renderer() - expected = renderer.render(InvertedLists()) - self.assertString(expected, u"""one, two, three, empty list""") + actual = renderer.render(InvertedLists()) + self.assertString(actual, u"""one, two, three, empty list""") class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): -- cgit v1.2.1 From 08c4032b6be9847d24a63bc04293f6d24bb451c5 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 19:54:53 -0700 Subject: Removed View dependency from more examples and test cases. --- examples/partials_with_lambdas.py | 6 ++---- examples/simple.py | 5 ++--- examples/template_partial.py | 13 +++++++++---- tests/common.py | 4 ++++ tests/test_examples.py | 25 ++++++++++++++++++------- tests/test_simple.py | 31 ++++++++++++++++++++----------- tests/test_template_spec.py | 24 +++++++++++++++--------- 7 files changed, 70 insertions(+), 38 deletions(-) diff --git a/examples/partials_with_lambdas.py b/examples/partials_with_lambdas.py index 4c3ee97..463a3ce 100644 --- a/examples/partials_with_lambdas.py +++ b/examples/partials_with_lambdas.py @@ -1,8 +1,6 @@ -import pystache from examples.lambdas import rot -class PartialsWithLambdas(pystache.View): - template_path = 'examples' - +class PartialsWithLambdas(object): + def rot(self): return rot \ No newline at end of file diff --git a/examples/simple.py b/examples/simple.py index 792b243..3252a81 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,7 +1,6 @@ -import pystache +from pystache import TemplateSpec -class Simple(pystache.View): - template_path = 'examples' +class Simple(TemplateSpec): def thing(self): return "pizza" diff --git a/examples/template_partial.py b/examples/template_partial.py index d3dbfd8..e96c83b 100644 --- a/examples/template_partial.py +++ b/examples/template_partial.py @@ -1,7 +1,12 @@ -import pystache +from pystache import TemplateSpec -class TemplatePartial(pystache.View): - template_path = 'examples' +class TemplatePartial(TemplateSpec): + + def __init__(self, renderer): + self.renderer = renderer + + def _context_get(self, key): + return self.renderer.context.get(key) def title(self): return "Welcome" @@ -13,4 +18,4 @@ class TemplatePartial(pystache.View): return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] def thing(self): - return self.context.get('prop') \ No newline at end of file + return self._context_get('prop') \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 603472a..adc3ec2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -7,8 +7,12 @@ Provides test-related code that can be used by all tests. import os +import examples + DATA_DIR = 'tests/data' +EXAMPLES_DIR = os.path.dirname(examples.__file__) + def get_data_path(file_name): return os.path.join(DATA_DIR, file_name) diff --git a/tests/test_examples.py b/tests/test_examples.py index b7df909..5b5af01 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -12,7 +12,8 @@ from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext from pystache import Renderer -from tests.common import AssertStringMixin +from .common import EXAMPLES_DIR +from .common import AssertStringMixin class TestView(unittest.TestCase, AssertStringMixin): @@ -42,13 +43,19 @@ class TestView(unittest.TestCase, AssertStringMixin): self.assertEquals(Unescaped().render(), "

    Bear > Shark

    ") def test_template_partial(self): - self.assertEquals(TemplatePartial().render(), """

    Welcome

    + renderer = Renderer(search_dirs=EXAMPLES_DIR) + actual = renderer.render(TemplatePartial(renderer=renderer)) + + self.assertString(actual, u"""

    Welcome

    Again, Welcome!""") def test_template_partial_extension(self): - view = TemplatePartial() - view.template_extension = 'txt' - self.assertString(view.render(), u"""Welcome + renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') + + view = TemplatePartial(renderer=renderer) + + actual = renderer.render(view) + self.assertString(actual, u"""Welcome ------- ## Again, Welcome! ##""") @@ -77,9 +84,13 @@ Again, Welcome!""") self.assertString(actual, u'it works!') def test_partial_in_partial_has_access_to_grand_parent_context(self): - view = TemplatePartial(context = {'prop': 'derp'}) + renderer = Renderer(search_dirs=EXAMPLES_DIR) + + view = TemplatePartial(renderer=renderer) view.template = '''{{>partial_in_partial}}''' - self.assertEquals(view.render(), 'Hi derp!') + + actual = renderer.render(view, {'prop': 'derp'}) + self.assertEquals(actual, 'Hi derp!') if __name__ == '__main__': unittest.main() diff --git a/tests/test_simple.py b/tests/test_simple.py index aba0e89..0a8a83c 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -8,7 +8,8 @@ from examples.lambdas import Lambdas from examples.template_partial import TemplatePartial from examples.simple import Simple -from tests.common import AssertStringMixin +from .common import EXAMPLES_DIR +from .common import AssertStringMixin class TestSimple(unittest.TestCase, AssertStringMixin): @@ -26,8 +27,8 @@ class TestSimple(unittest.TestCase, AssertStringMixin): context = Complex() renderer = Renderer() - expected = renderer.render(template, context) - self.assertEquals(expected, "Colors: red Colors: green Colors: blue ") + actual = renderer.render(template, context) + self.assertEquals(actual, "Colors: red Colors: green Colors: blue ") def test_empty_context(self): template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' @@ -38,16 +39,21 @@ class TestSimple(unittest.TestCase, AssertStringMixin): view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}' renderer = Renderer() - expected = renderer.render(view) - self.assertString(expected, u'bar != bar. oh, it does!') + actual = renderer.render(view) + self.assertString(actual, u'bar != bar. oh, it does!') def test_rendering_partial(self): - view = TemplatePartial() + renderer = Renderer(search_dirs=EXAMPLES_DIR) + + view = TemplatePartial(renderer=renderer) view.template = '{{>inner_partial}}' - self.assertEquals(view.render(), 'Again, Welcome!') + + actual = renderer.render(view) + self.assertString(actual, u'Again, Welcome!') view.template = '{{#looping}}{{>inner_partial}} {{/looping}}' - self.assertEquals(view.render(), '''Again, Welcome! Again, Welcome! Again, Welcome! ''') + actual = renderer.render(view) + self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ") def test_non_existent_value_renders_blank(self): view = Simple() @@ -66,9 +72,12 @@ class TestSimple(unittest.TestCase, AssertStringMixin): In particular, this means that trailing newlines should be removed. """ - view = TemplatePartial() - view.template_extension = 'txt' - self.assertString(view.render(), u"""Welcome + renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') + + view = TemplatePartial(renderer=renderer) + + actual = renderer.render(view) + self.assertString(actual, u"""Welcome ------- ## Again, Welcome! ##""") diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 692f93e..a85bd53 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -20,16 +20,14 @@ from pystache import View from pystache.template_spec import SpecLoader from pystache.locator import Locator from pystache.loader import Loader +from .common import DATA_DIR +from .common import EXAMPLES_DIR from .common import AssertIsMixin from .common import AssertStringMixin -from .common import DATA_DIR from .data.views import SampleView from .data.views import NonAscii -EXAMPLES_DIR = os.path.dirname(examples.__file__) - - class Thing(object): pass @@ -78,13 +76,18 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): self.assertEquals(view.render(), "Partial: No tags...") def test_basic_method_calls(self): - view = Simple() - self.assertEquals(view.render(), "Hi pizza!") + renderer = Renderer() + actual = renderer.render(Simple()) + + self.assertString(actual, u"Hi pizza!") def test_non_callable_attributes(self): view = Simple() view.thing = 'Chris' - self.assertEquals(view.render(), "Hi Chris!") + + renderer = Renderer() + actual = renderer.render(view) + self.assertEquals(actual, "Hi Chris!") def test_complex(self): renderer = Renderer() @@ -143,10 +146,13 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): parent = Thing() parent.this = 'derp' parent.children = [Thing()] - view = Simple(context={'parent': parent}) + view = Simple() view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" - self.assertEquals(view.render(), 'derp') + renderer = Renderer() + actual = renderer.render(view, {'parent': parent}) + + self.assertString(actual, u'derp') def test_inverted_lists(self): renderer = Renderer() -- cgit v1.2.1 From 8fdbaf27a65d63332841d8672ac9a8c6a0300a45 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 20:23:22 -0700 Subject: Removed View dependency from Unescaped example. --- examples/unescaped.py | 5 +---- tests/test_examples.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/unescaped.py b/examples/unescaped.py index a040dcb..67c12ca 100644 --- a/examples/unescaped.py +++ b/examples/unescaped.py @@ -1,7 +1,4 @@ -import pystache - -class Unescaped(pystache.View): - template_path = 'examples' +class Unescaped(object): def title(self): return "Bear > Shark" diff --git a/tests/test_examples.py b/tests/test_examples.py index 5b5af01..36b8f7e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -40,7 +40,9 @@ class TestView(unittest.TestCase, AssertStringMixin): self._assert(Escaped(), u"

    Bear > Shark

    ") def test_literal(self): - self.assertEquals(Unescaped().render(), "

    Bear > Shark

    ") + renderer = Renderer() + actual = renderer.render(Unescaped()) + self.assertEquals(actual, "

    Bear > Shark

    ") def test_template_partial(self): renderer = Renderer(search_dirs=EXAMPLES_DIR) -- cgit v1.2.1 From 49c1d7a9521ae9e20202ddb25d9b9d30ad85d016 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 20:28:08 -0700 Subject: Removed View dependency from last remaining examples. --- examples/unicode_input.mustache | 2 +- examples/unicode_input.py | 6 +++--- examples/unicode_output.py | 5 +---- tests/test_examples.py | 11 +++++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/unicode_input.mustache b/examples/unicode_input.mustache index 9b5e335..f654cd1 100644 --- a/examples/unicode_input.mustache +++ b/examples/unicode_input.mustache @@ -1 +1 @@ -

    If alive today, Henri Poincaré would be {{age}} years old.

    \ No newline at end of file +abcdé \ No newline at end of file diff --git a/examples/unicode_input.py b/examples/unicode_input.py index 9f3684f..2c10fcb 100644 --- a/examples/unicode_input.py +++ b/examples/unicode_input.py @@ -1,7 +1,7 @@ -import pystache +from pystache import TemplateSpec + +class UnicodeInput(TemplateSpec): -class UnicodeInput(pystache.View): - template_path = 'examples' template_encoding = 'utf8' def age(self): diff --git a/examples/unicode_output.py b/examples/unicode_output.py index 3cb9260..d5579c3 100644 --- a/examples/unicode_output.py +++ b/examples/unicode_output.py @@ -1,9 +1,6 @@ # encoding: utf-8 -import pystache - -class UnicodeOutput(pystache.View): - template_path = 'examples' +class UnicodeOutput(object): def name(self): return u'Henri Poincaré' diff --git a/tests/test_examples.py b/tests/test_examples.py index 36b8f7e..552fffd 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -30,11 +30,14 @@ class TestView(unittest.TestCase, AssertStringMixin): self._assert(DoubleSection(), u"* first\n* second\n* third") def test_unicode_output(self): - self.assertEquals(UnicodeOutput().render(), u'

    Name: Henri Poincaré

    ') + renderer = Renderer() + actual = renderer.render(UnicodeOutput()) + self.assertString(actual, u'

    Name: Henri Poincaré

    ') def test_unicode_input(self): - self.assertEquals(UnicodeInput().render(), - u'

    If alive today, Henri Poincaré would be 156 years old.

    ') + renderer = Renderer() + actual = renderer.render(UnicodeInput()) + self.assertString(actual, u'abcdé') def test_escaping(self): self._assert(Escaped(), u"

    Bear > Shark

    ") @@ -42,7 +45,7 @@ class TestView(unittest.TestCase, AssertStringMixin): def test_literal(self): renderer = Renderer() actual = renderer.render(Unescaped()) - self.assertEquals(actual, "

    Bear > Shark

    ") + self.assertString(actual, u"

    Bear > Shark

    ") def test_template_partial(self): renderer = Renderer(search_dirs=EXAMPLES_DIR) -- cgit v1.2.1 From e87e859f524031c20fff5aeea5ec00cd00cc1664 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 20:59:30 -0700 Subject: Removed View class (finally!). --- pystache/init.py | 3 +- pystache/template_spec.py | 3 +- pystache/view.py | 82 --------------------------------------------- tests/test_template_spec.py | 55 +++++++++++++----------------- 4 files changed, 25 insertions(+), 118 deletions(-) delete mode 100644 pystache/view.py diff --git a/pystache/init.py b/pystache/init.py index b629150..f227f78 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -7,10 +7,9 @@ This module contains the initialization logic called by __init__.py. from .renderer import Renderer from .template_spec import TemplateSpec -from .view import View -__all__ = ['render', 'Renderer', 'View', 'TemplateSpec'] +__all__ = ['render', 'Renderer', 'TemplateSpec'] def render(template, context=None, **kwargs): diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 68dc52d..1e3695f 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -52,8 +52,7 @@ class TemplateSpec(object): template_encoding = None -# TODO: add test cases for this class, and then refactor while replacing the -# View class. +# TODO: add test cases for this class. class SpecLoader(object): """ diff --git a/pystache/view.py b/pystache/view.py deleted file mode 100644 index 1aa31be..0000000 --- a/pystache/view.py +++ /dev/null @@ -1,82 +0,0 @@ -# coding: utf-8 - -""" -This module exposes the deprecated View class. - -TODO: remove this module. - -""" - -from .context import Context -from .locator import Locator -from .renderer import Renderer -from .template_spec import TemplateSpec - - -# TODO: remove this class. -class View(TemplateSpec): - - _renderer = None - - locator = Locator() - - def __init__(self, context=None): - """ - Construct a View instance. - - """ - context = Context.create(self, context) - - self.context = context - - def _get_renderer(self): - if self._renderer is None: - # We delay setting self._renderer until now (instead of, say, - # setting it in the constructor) in case the user changes after - # instantiation some of the attributes on which the Renderer - # depends. This lets users set the template_extension attribute, - # etc. after View.__init__() has already been called. - renderer = Renderer(file_encoding=self.template_encoding, - search_dirs=self.template_path, - file_extension=self.template_extension) - self._renderer = renderer - - return self._renderer - - def get_template(self): - """ - Return the current template after setting it, if necessary. - - """ - if not self.template: - template_name = self._get_template_name() - renderer = self._get_renderer() - self.template = renderer.load_template(template_name) - - return self.template - - def _get_template_name(self): - """ - Return the name of the template to load. - - If the template_name attribute is not set, then this method constructs - the template name from the class name as follows, for example: - - TemplatePartial => template_partial - - Otherwise, this method returns the template_name. - - """ - if self.template_name: - return self.template_name - - return self.locator.make_template_name(self) - - def render(self): - """ - Return the view rendered using the current context. - - """ - template = self.get_template() - renderer = self._get_renderer() - return renderer.render(template, self.context) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index a85bd53..6f9b9eb 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -14,9 +14,8 @@ from examples.simple import Simple from examples.complex import Complex from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists -from pystache import TemplateSpec as Template +from pystache import TemplateSpec from pystache import Renderer -from pystache import View from pystache.template_spec import SpecLoader from pystache.locator import Locator from pystache.loader import Loader @@ -34,46 +33,38 @@ class Thing(object): class ViewTestCase(unittest.TestCase, AssertStringMixin): - def test_init(self): + def test_template_rel_directory(self): """ - Test the constructor. + Test that View.template_rel_directory is respected. """ - class TestView(View): - template = "foo" - - view = TestView() - self.assertEquals(view.template, "foo") - - def test_template_path(self): - """ - Test that View.template_path is respected. - - """ - class Tagless(View): + class Tagless(TemplateSpec): pass view = Tagless() - self.assertRaises(IOError, view.render) + renderer = Renderer() - view = Tagless() - view.template_path = "examples" - self.assertEquals(view.render(), "No tags...") + self.assertRaises(IOError, renderer.render, view) + + view.template_rel_directory = "../examples" + actual = renderer.render(view) + self.assertEquals(actual, "No tags...") def test_template_path_for_partials(self): """ Test that View.template_rel_path is respected for partials. """ - class TestView(View): - template = "Partial: {{>tagless}}" + spec = TemplateSpec() + spec.template = "Partial: {{>tagless}}" + + renderer1 = Renderer() + renderer2 = Renderer(search_dirs=EXAMPLES_DIR) - view = TestView() - self.assertRaises(IOError, view.render) + self.assertRaises(IOError, renderer1.render, spec) - view = TestView() - view.template_path = "examples" - self.assertEquals(view.render(), "Partial: No tags...") + actual = renderer2.render(spec) + self.assertEquals(actual, "Partial: No tags...") def test_basic_method_calls(self): renderer = Renderer() @@ -192,7 +183,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): Test the template attribute: str string. """ - custom = Template() + custom = TemplateSpec() custom.template = "abc" self._assert_template(SpecLoader(), custom, u"abc") @@ -202,7 +193,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): Test the template attribute: unicode string. """ - custom = Template() + custom = TemplateSpec() custom.template = u"abc" self._assert_template(SpecLoader(), custom, u"abc") @@ -212,7 +203,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): Test the template attribute: non-ascii unicode string. """ - custom = Template() + custom = TemplateSpec() custom.template = u"é" self._assert_template(SpecLoader(), custom, u"é") @@ -222,7 +213,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): Test the template attribute: with template encoding attribute. """ - custom = Template() + custom = TemplateSpec() custom.template = u'é'.encode('utf-8') self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é') @@ -257,7 +248,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): custom_loader = SpecLoader() custom_loader.loader = loader - view = Template() + view = TemplateSpec() view.template = "template-foo" view.template_encoding = "encoding-foo" -- cgit v1.2.1 From a984321161ab760ac60514b50596d8147714e454 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 21:01:57 -0700 Subject: Removed TemplateSpec.template_path. --- pystache/template_spec.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 1e3695f..4d042fa 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -43,8 +43,6 @@ class TemplateSpec(object): """ template = None - # TODO: remove template_path. - template_path = None template_rel_path = None template_rel_directory = None template_name = None -- cgit v1.2.1 From ad3707a030293dec100b92d7e7b1242d18e4cf7c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 21:06:26 -0700 Subject: Moved the SpecLoader class into its own spec_loader module. --- pystache/renderer.py | 2 +- pystache/spec_loader.py | 84 +++++++++++++++++++++++++++++++++++++++++++++ pystache/template_spec.py | 81 ------------------------------------------- tests/test_template_spec.py | 3 +- 4 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 pystache/spec_loader.py diff --git a/pystache/renderer.py b/pystache/renderer.py index a02693c..9a0a1b3 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -9,7 +9,7 @@ from . import defaults from .context import Context from .loader import Loader from .renderengine import RenderEngine -from .template_spec import SpecLoader +from .spec_loader import SpecLoader from .template_spec import TemplateSpec diff --git a/pystache/spec_loader.py b/pystache/spec_loader.py new file mode 100644 index 0000000..e18140b --- /dev/null +++ b/pystache/spec_loader.py @@ -0,0 +1,84 @@ +# coding: utf-8 + +""" +This module supports customized (aka special or specified) template loading. + +""" + +import os.path + +from .loader import Loader + + +# TODO: add test cases for this class. +class SpecLoader(object): + + """ + Supports loading custom-specified templates (from TemplateSpec instances). + + """ + + def __init__(self, loader=None): + if loader is None: + loader = Loader() + + self.loader = loader + + def _find_relative(self, spec): + """ + Return the path to the template as a relative (dir, file_name) pair. + + The directory returned is relative to the directory containing the + class definition of the given object. The method returns None for + this directory if the directory is unknown without first searching + the search directories. + + """ + if spec.template_rel_path is not None: + return os.path.split(spec.template_rel_path) + + # Otherwise, determine the file name separately. + locator = self.loader._make_locator() + + template_name = (spec.template_name if spec.template_name is not None else + locator.make_template_name(spec)) + + file_name = locator.make_file_name(template_name, spec.template_extension) + + return (spec.template_rel_directory, file_name) + + def _find(self, spec): + """ + Find and return the path to the template associated to the instance. + + """ + dir_path, file_name = self._find_relative(spec) + + locator = self.loader._make_locator() + + if dir_path is None: + # Then we need to search for the path. + path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) + else: + obj_dir = locator.get_object_directory(spec) + path = os.path.join(obj_dir, dir_path, file_name) + + return path + + def load(self, spec): + """ + Find and return the template associated to a TemplateSpec instance. + + Returns the template as a unicode string. + + Arguments: + + spec: a TemplateSpec instance. + + """ + if spec.template is not None: + return self.loader.unicode(spec.template, spec.template_encoding) + + path = self._find(spec) + + return self.loader.read(path, spec.template_encoding) diff --git a/pystache/template_spec.py b/pystache/template_spec.py index 4d042fa..c33f30b 100644 --- a/pystache/template_spec.py +++ b/pystache/template_spec.py @@ -5,13 +5,6 @@ This module supports customized (aka special or specified) template loading. """ -import os.path - -from .loader import Loader - - -# TODO: consider putting TemplateSpec and SpecLoader in separate modules. - # TODO: finish the class docstring. class TemplateSpec(object): @@ -48,77 +41,3 @@ class TemplateSpec(object): template_name = None template_extension = None template_encoding = None - - -# TODO: add test cases for this class. -class SpecLoader(object): - - """ - Supports loading a custom-specified template. - - """ - - def __init__(self, loader=None): - if loader is None: - loader = Loader() - - self.loader = loader - - def _find_relative(self, spec): - """ - Return the path to the template as a relative (dir, file_name) pair. - - The directory returned is relative to the directory containing the - class definition of the given object. The method returns None for - this directory if the directory is unknown without first searching - the search directories. - - """ - if spec.template_rel_path is not None: - return os.path.split(spec.template_rel_path) - - # Otherwise, determine the file name separately. - locator = self.loader._make_locator() - - template_name = (spec.template_name if spec.template_name is not None else - locator.make_template_name(spec)) - - file_name = locator.make_file_name(template_name, spec.template_extension) - - return (spec.template_rel_directory, file_name) - - def _find(self, spec): - """ - Find and return the path to the template associated to the instance. - - """ - dir_path, file_name = self._find_relative(spec) - - locator = self.loader._make_locator() - - if dir_path is None: - # Then we need to search for the path. - path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) - else: - obj_dir = locator.get_object_directory(spec) - path = os.path.join(obj_dir, dir_path, file_name) - - return path - - def load(self, spec): - """ - Find and return the template associated to a TemplateSpec instance. - - Returns the template as a unicode string. - - Arguments: - - spec: a TemplateSpec instance. - - """ - if spec.template is not None: - return self.loader.unicode(spec.template, spec.template_encoding) - - path = self._find(spec) - - return self.loader.read(path, spec.template_encoding) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 6f9b9eb..35861d2 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -16,9 +16,10 @@ from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists from pystache import TemplateSpec from pystache import Renderer -from pystache.template_spec import SpecLoader +from pystache.spec_loader import SpecLoader from pystache.locator import Locator from pystache.loader import Loader +from pystache.spec_loader import SpecLoader from .common import DATA_DIR from .common import EXAMPLES_DIR from .common import AssertIsMixin -- cgit v1.2.1 From b59db4e8b7e25208c3ed5c0bd6e8224469d2283c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 21:10:12 -0700 Subject: Tweaked import statements. --- tests/test_template_spec.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 35861d2..7d97af0 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -14,9 +14,8 @@ from examples.simple import Simple from examples.complex import Complex from examples.lambdas import Lambdas from examples.inverted import Inverted, InvertedLists -from pystache import TemplateSpec from pystache import Renderer -from pystache.spec_loader import SpecLoader +from pystache import TemplateSpec from pystache.locator import Locator from pystache.loader import Loader from pystache.spec_loader import SpecLoader -- cgit v1.2.1 From 51d636fc776b3cbeee0b01b1256f8828e34b0c26 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 21:23:53 -0700 Subject: Bump version to release candidate v0.5.0-rc. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e96230b..38c65f1 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ if sys.argv[-1] == 'publish': long_description = make_long_description() setup(name='pystache', - version='0.4.1', + version='0.5.0-rc', description='Mustache for Python', long_description=long_description, author='Chris Wanstrath', -- cgit v1.2.1 From 4d85efd6b18648a8d749b0cfd5e370a35b8e37d7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 21:26:52 -0700 Subject: Added maintainer field to setup.py. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 38c65f1..65d8d6f 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup(name='pystache', long_description=long_description, author='Chris Wanstrath', author_email='chris@ozmm.org', + maintainer='Chris Jerdonek', url='http://github.com/defunkt/pystache', packages=['pystache'], license='MIT', -- cgit v1.2.1 From 4d8c4d7a89088e0d6e5fdd3e83d901a63251d539 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 22:00:06 -0700 Subject: Added an intro paragraph to the new section of the HISTORY file. --- HISTORY.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8ca8169..3772d57 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,8 +1,15 @@ History ======= -Next Release (version TBD) --------------------------- +0.5.0 (TBD) +----------- + +This version represents a major rewrite and refactoring of the code base +that also adds features and fixes many bugs. All functionality and nearly +all unit tests have been preserved. However, some backwards incompatible +changes have been made to the API. + +TODO: add a section describing key changes. Features: -- cgit v1.2.1 From 50bd19dc9735c00a90b9f830842a2cddbcf19742 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 23:15:49 -0700 Subject: Started adding list of key changes to HISTORY file. --- HISTORY.rst | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3772d57..3041c51 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,9 +7,22 @@ History This version represents a major rewrite and refactoring of the code base that also adds features and fixes many bugs. All functionality and nearly all unit tests have been preserved. However, some backwards incompatible -changes have been made to the API. - -TODO: add a section describing key changes. +changes to the API have been made. + +Key Changes: + +* Pystache now passes all tests in version 1.0.3 of the Mustache spec. [pvande] +* Removed View class: it is no longer necessary to subclass from the View class + or any class to create a view. +* Replaced Template with Renderer class: template rendering behavior can + be modified via the Renderer constructor or by setting attributes on a Renderer instance. +* Added TemplateSpec class: template rendering can be specified on a per-view + basis by subclassing from TemplateSpec. +* Removed circular dependencies between modules (e.g. between Template and View) + and introduced separation of concerns (cf. issue #13). +* Unicode now used consistently throughout the rendering process. + +TODO: complete the list of key changes. Features: -- cgit v1.2.1 From c18be2d746e28167f39afbbf3fc27cec0a61e734 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 23:20:26 -0700 Subject: Annotated HISTORY file with two links. --- HISTORY.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3041c51..20a58b6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,15 +11,15 @@ changes to the API have been made. Key Changes: -* Pystache now passes all tests in version 1.0.3 of the Mustache spec. [pvande] -* Removed View class: it is no longer necessary to subclass from the View class - or any class to create a view. +* Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande] +* Removed View class: it is no longer necessary to subclass from View or + any class to create a view. * Replaced Template with Renderer class: template rendering behavior can be modified via the Renderer constructor or by setting attributes on a Renderer instance. * Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. * Removed circular dependencies between modules (e.g. between Template and View) - and introduced separation of concerns (cf. issue #13). + and introduced separation of concerns (cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. TODO: complete the list of key changes. @@ -102,3 +102,7 @@ Misc: ------------------ * First release + + +.. _issue #13: https://github.com/defunkt/pystache/issues/13 +.. _Mustache spec: https://github.com/mustache/spec -- cgit v1.2.1 From 006ddc84ff84e429d81759a1e8656487e4decee2 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 23:24:04 -0700 Subject: Tweak to HISTORY file. --- HISTORY.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 20a58b6..6b068ce 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,8 +18,8 @@ Key Changes: be modified via the Renderer constructor or by setting attributes on a Renderer instance. * Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. -* Removed circular dependencies between modules (e.g. between Template and View) - and introduced separation of concerns (cf. `issue #13`_). +* Introduced separation of concerns between modules and removed circular + dependencies (e.g. between Template and View, cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. TODO: complete the list of key changes. -- cgit v1.2.1 From bf21ce3c99bf5184c299594f465f64061b636126 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 1 Apr 2012 23:30:50 -0700 Subject: More tweaks to HISTORY. --- HISTORY.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6b068ce..f5ba6a1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,13 +13,13 @@ Key Changes: * Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande] * Removed View class: it is no longer necessary to subclass from View or - any class to create a view. + from any class to create a view. * Replaced Template with Renderer class: template rendering behavior can be modified via the Renderer constructor or by setting attributes on a Renderer instance. * Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. -* Introduced separation of concerns between modules and removed circular - dependencies (e.g. between Template and View, cf. `issue #13`_). +* Introduced separation of concerns and removed circular dependencies (e.g. + between Template and View classes, cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. TODO: complete the list of key changes. -- cgit v1.2.1 From b5ec7e930b83d49defe51c380f278049c7ca673c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 05:27:25 -0700 Subject: Removed some now-redundant information from HISTORY file. --- HISTORY.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f5ba6a1..e949983 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,7 +13,7 @@ Key Changes: * Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande] * Removed View class: it is no longer necessary to subclass from View or - from any class to create a view. + from any other class to create a view. * Replaced Template with Renderer class: template rendering behavior can be modified via the Renderer constructor or by setting attributes on a Renderer instance. * Added TemplateSpec class: template rendering can be specified on a per-view @@ -29,7 +29,6 @@ Features: * Views and Renderers accept a custom template loader. Also, this loader can be a dictionary of partials. [cjerdonek] * Added a command-line interface. [vrde, cjerdonek] -* Markupsafe can now be disabled after import. [cjerdonek] * Custom escape function can now be passed to Template constructor. [cjerdonek] * Template class can now handle non-ascii characters in non-unicode strings. Added default_encoding and decode_errors to Template constructor arguments. @@ -38,8 +37,6 @@ Features: API changes: -* Template class replaced by a Renderer class. [cjerdonek] -* ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek] * Removed output_encoding options. [cjerdonek] * Removed automatic use of markupsafe, if available. [cjerdonek] -- cgit v1.2.1 From d202377f2a356c5e2485af61372a87a904cbb844 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 13:34:32 -0700 Subject: Added to README a section on unicode string handling. --- README.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.rst b/README.rst index 39f3840..ae3ef8f 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,33 @@ Pull it together:: u'Hello, Pizza!' +Unicode +======= + +This section describes Pystache's handling of unicode, strings, and encodings. +Pystache's template rendering methods output only unicode. Moreover, +internally Pystache uses only unicode strings. For input, Pystache accepts +both ``unicode`` and ``str`` strings. + +The Renderer class supports a number of attributes that control how it +converts ``str`` strings to unicode on input. These attributes include +the file_encoding, string_encoding, and decode_errors attributes. + +The file_encoding attribute is the encoding the renderer uses to convert +to unicode any files read from the file system. Similarly, string_encoding +is the encoding the renderer uses to convert any other strings of type str +to unicode (e.g. context values of type ``str``). The decode_errors +attribute is what the renderer passes as the ``errors`` argument to +Python's built-in `unicode()`_ function. The valid values are 'strict', +'ignore', or 'replace'. + +Each of these attributes can be set via the Renderer class's constructor +using a keyword argument of the same name. In addition, the file_encoding +attribute can be controlled on a per-view basis by subclassing the +`TemplateSpec` class. The attributes default to values set in Pystache's +``defaults`` module. + + Test It ======= @@ -136,4 +163,5 @@ Author .. _PyPI: http://pypi.python.org/pypi/pystache .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org +.. _unicode(): http://docs.python.org/library/functions.html#unicode .. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From a3173b69fab14e3456ea9b7cf0679701e81e701a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 13:34:32 -0700 Subject: Added a few more items to the HISTORY file. --- HISTORY.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e949983..7c5ae4a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,18 +9,21 @@ that also adds features and fixes many bugs. All functionality and nearly all unit tests have been preserved. However, some backwards incompatible changes to the API have been made. -Key Changes: +Highlights: * Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande] * Removed View class: it is no longer necessary to subclass from View or from any other class to create a view. -* Replaced Template with Renderer class: template rendering behavior can - be modified via the Renderer constructor or by setting attributes on a Renderer instance. +* Replaced Template with Renderer class: template rendering behavior can be + modified via the Renderer constructor or by setting attributes on a Renderer instance. * Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. * Introduced separation of concerns and removed circular dependencies (e.g. between Template and View classes, cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. +* Expanded test coverage: nosetests now includes doctests and ~105 test cases + from the Mustache spec (for a total of ~315 unit tests up from 56). +* Added rudimentary benchmarking script. TODO: complete the list of key changes. -- cgit v1.2.1 From cf2f05debd473623fbb8fcfae15ed5519ebbfe88 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 13:34:33 -0700 Subject: Added two TODO's to the README. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index ae3ef8f..51d8c01 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,9 @@ Pystache currently works using the following versions of Python: * Python 2.6 * Python 2.7 +TODO: mention simplejson for earlier versions of Python and yaml. +TODO: try to replace yaml with json. + Install It ========== -- cgit v1.2.1 From ccee7d9e1bc705df6e2b0e2abf7133850a727b2b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 18:30:29 -0700 Subject: Tweaks to the unicode section of the README. --- README.rst | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 51d8c01..23f88d7 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,7 @@ Pystache currently works using the following versions of Python: * Python 2.7 TODO: mention simplejson for earlier versions of Python and yaml. + TODO: try to replace yaml with json. @@ -76,31 +77,33 @@ Pull it together:: u'Hello, Pizza!' -Unicode -======= +Unicode Handling +================ + +This section describes Pystache's handling of unicode (e.g. strings and +encodings). -This section describes Pystache's handling of unicode, strings, and encodings. -Pystache's template rendering methods output only unicode. Moreover, -internally Pystache uses only unicode strings. For input, Pystache accepts -both ``unicode`` and ``str`` strings. +Internally Pystache uses only unicode strings. For input, Pystache accepts +both ``unicode`` and ``str`` strings. For output, Pystache's template +rendering methods return only unicode. -The Renderer class supports a number of attributes that control how it -converts ``str`` strings to unicode on input. These attributes include -the file_encoding, string_encoding, and decode_errors attributes. +The Renderer class supports a number of attributes that control how Pystache +converts ``str`` strings to unicode on input. These include the +file_encoding, string_encoding, and decode_errors attributes. The file_encoding attribute is the encoding the renderer uses to convert -to unicode any files read from the file system. Similarly, string_encoding +any files read from the file system to unicode. Similarly, string_encoding is the encoding the renderer uses to convert any other strings of type str to unicode (e.g. context values of type ``str``). The decode_errors attribute is what the renderer passes as the ``errors`` argument to -Python's built-in `unicode()`_ function. The valid values are 'strict', -'ignore', or 'replace'. +Python's built-in `unicode()`_ function when converting. The valid values +are 'strict', 'ignore', or 'replace'. Each of these attributes can be set via the Renderer class's constructor using a keyword argument of the same name. In addition, the file_encoding -attribute can be controlled on a per-view basis by subclassing the -`TemplateSpec` class. The attributes default to values set in Pystache's -``defaults`` module. +attribute can be controlled on a per-view basis by subclassing the class +`TemplateSpec`. When not specified explicitly, these attributes default +to values set in Pystache's ``defaults`` module. Test It -- cgit v1.2.1 From 135540b4f7e07d0fcf92a1e4848a63c2d582d2de Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 19:03:21 -0700 Subject: More tweaks to the unicode section of the README. --- README.rst | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 23f88d7..c17ac63 100644 --- a/README.rst +++ b/README.rst @@ -83,26 +83,27 @@ Unicode Handling This section describes Pystache's handling of unicode (e.g. strings and encodings). -Internally Pystache uses only unicode strings. For input, Pystache accepts +Internally, Pystache uses only unicode strings. For input, Pystache accepts both ``unicode`` and ``str`` strings. For output, Pystache's template rendering methods return only unicode. -The Renderer class supports a number of attributes that control how Pystache -converts ``str`` strings to unicode on input. These include the -file_encoding, string_encoding, and decode_errors attributes. - -The file_encoding attribute is the encoding the renderer uses to convert -any files read from the file system to unicode. Similarly, string_encoding -is the encoding the renderer uses to convert any other strings of type str -to unicode (e.g. context values of type ``str``). The decode_errors -attribute is what the renderer passes as the ``errors`` argument to -Python's built-in `unicode()`_ function when converting. The valid values -are 'strict', 'ignore', or 'replace'. - -Each of these attributes can be set via the Renderer class's constructor -using a keyword argument of the same name. In addition, the file_encoding -attribute can be controlled on a per-view basis by subclassing the class -`TemplateSpec`. When not specified explicitly, these attributes default +Pystache's ``Renderer`` class supports a number of attributes that control how +Pystache converts ``str`` strings to unicode on input. These include the +``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes. + +The ``file_encoding`` attribute is the encoding the renderer uses to convert +any files read from the file system to unicode. Similarly, ``string_encoding`` +is the encoding the renderer uses to convert to unicode any other strings of +type ``str`` encountered during the rendering process (e.g. context values +of type ``str``). The ``decode_errors`` attribute is what the renderer +passes as the ``errors`` argument to Python's `built-in unicode function`_ +``unicode()`` when converting. The valid values for this argument are +``strict``, ``ignore``, and ``replace``. + +Each of these attributes can be set via the ``Renderer`` class's constructor +using a keyword argument of the same name. In addition, the ``file_encoding`` +attribute can be controlled on a per-view basis by subclassing the +``TemplateSpec`` class. When not specified explicitly, these attributes default to values set in Pystache's ``defaults`` module. @@ -169,5 +170,5 @@ Author .. _PyPI: http://pypi.python.org/pypi/pystache .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org -.. _unicode(): http://docs.python.org/library/functions.html#unicode +.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode .. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From b2434c025c7052ed265f04ab12fabd671f9498db Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 05:53:19 -0700 Subject: Changed relative imports to absolute for Python 2.4 support. --- pystache/init.py | 4 ++-- pystache/loader.py | 2 +- pystache/renderer.py | 10 +++++----- pystache/spec_loader.py | 2 +- tests/test_examples.py | 4 ++-- tests/test_loader.py | 2 +- tests/test_locator.py | 2 +- tests/test_renderer.py | 6 +++--- tests/test_simple.py | 4 ++-- tests/test_template_spec.py | 12 ++++++------ 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pystache/init.py b/pystache/init.py index f227f78..b285a5c 100644 --- a/pystache/init.py +++ b/pystache/init.py @@ -5,8 +5,8 @@ This module contains the initialization logic called by __init__.py. """ -from .renderer import Renderer -from .template_spec import TemplateSpec +from pystache.renderer import Renderer +from pystache.template_spec import TemplateSpec __all__ = ['render', 'Renderer', 'TemplateSpec'] diff --git a/pystache/loader.py b/pystache/loader.py index cc15d23..71961c3 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -11,7 +11,7 @@ import os import sys from . import defaults -from .locator import Locator +from pystache.locator import Locator def _to_unicode(s, encoding=None): diff --git a/pystache/renderer.py b/pystache/renderer.py index 9a0a1b3..94edf99 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -6,11 +6,11 @@ This module provides a Renderer class to render templates. """ from . import defaults -from .context import Context -from .loader import Loader -from .renderengine import RenderEngine -from .spec_loader import SpecLoader -from .template_spec import TemplateSpec +from pystache.context import Context +from pystache.loader import Loader +from pystache.renderengine import RenderEngine +from pystache.spec_loader import SpecLoader +from pystache.template_spec import TemplateSpec class Renderer(object): diff --git a/pystache/spec_loader.py b/pystache/spec_loader.py index e18140b..ac706a5 100644 --- a/pystache/spec_loader.py +++ b/pystache/spec_loader.py @@ -7,7 +7,7 @@ This module supports customized (aka special or specified) template loading. import os.path -from .loader import Loader +from pystache.loader import Loader # TODO: add test cases for this class. diff --git a/tests/test_examples.py b/tests/test_examples.py index 552fffd..179b089 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -12,8 +12,8 @@ from examples.unicode_output import UnicodeOutput from examples.unicode_input import UnicodeInput from examples.nested_context import NestedContext from pystache import Renderer -from .common import EXAMPLES_DIR -from .common import AssertStringMixin +from tests.common import EXAMPLES_DIR +from tests.common import AssertStringMixin class TestView(unittest.TestCase, AssertStringMixin): diff --git a/tests/test_loader.py b/tests/test_loader.py index a285e68..119ebef 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -9,7 +9,7 @@ import os import sys import unittest -from .common import AssertStringMixin +from tests.common import AssertStringMixin from pystache import defaults from pystache.loader import Loader diff --git a/tests/test_locator.py b/tests/test_locator.py index fc33a2f..94a55ad 100644 --- a/tests/test_locator.py +++ b/tests/test_locator.py @@ -14,7 +14,7 @@ import unittest from pystache.loader import Loader as Reader from pystache.locator import Locator -from .common import DATA_DIR +from tests.common import DATA_DIR from data.views import SayHello diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 6737cb1..a69d11a 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -15,9 +15,9 @@ from pystache import Renderer from pystache import TemplateSpec from pystache.loader import Loader -from .common import get_data_path -from .common import AssertStringMixin -from .data.views import SayHello +from tests.common import get_data_path +from tests.common import AssertStringMixin +from tests.data.views import SayHello class RendererInitTestCase(unittest.TestCase): diff --git a/tests/test_simple.py b/tests/test_simple.py index 0a8a83c..e19187f 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -8,8 +8,8 @@ from examples.lambdas import Lambdas from examples.template_partial import TemplatePartial from examples.simple import Simple -from .common import EXAMPLES_DIR -from .common import AssertStringMixin +from tests.common import EXAMPLES_DIR +from tests.common import AssertStringMixin class TestSimple(unittest.TestCase, AssertStringMixin): diff --git a/tests/test_template_spec.py b/tests/test_template_spec.py index 7d97af0..9599c37 100644 --- a/tests/test_template_spec.py +++ b/tests/test_template_spec.py @@ -19,12 +19,12 @@ from pystache import TemplateSpec from pystache.locator import Locator from pystache.loader import Loader from pystache.spec_loader import SpecLoader -from .common import DATA_DIR -from .common import EXAMPLES_DIR -from .common import AssertIsMixin -from .common import AssertStringMixin -from .data.views import SampleView -from .data.views import NonAscii +from tests.common import DATA_DIR +from tests.common import EXAMPLES_DIR +from tests.common import AssertIsMixin +from tests.common import AssertStringMixin +from tests.data.views import SampleView +from tests.data.views import NonAscii class Thing(object): -- cgit v1.2.1 From fc9f9659079bee827cc5dda96e9b09bbf9583cc7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 05:56:10 -0700 Subject: Changed more relative imports to absolute for Python 2.4 support. --- pystache/loader.py | 2 +- pystache/locator.py | 2 +- pystache/renderer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 71961c3..3f049a4 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -10,7 +10,7 @@ from __future__ import with_statement import os import sys -from . import defaults +from pystache import defaults from pystache.locator import Locator diff --git a/pystache/locator.py b/pystache/locator.py index e288049..a1f06db 100644 --- a/pystache/locator.py +++ b/pystache/locator.py @@ -9,7 +9,7 @@ import os import re import sys -from . import defaults +from pystache import defaults class Locator(object): diff --git a/pystache/renderer.py b/pystache/renderer.py index 94edf99..86d349f 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -5,7 +5,7 @@ This module provides a Renderer class to render templates. """ -from . import defaults +from pystache import defaults from pystache.context import Context from pystache.loader import Loader from pystache.renderengine import RenderEngine -- cgit v1.2.1 From e0f1403939c4326ac99aff9ae84d375f76fbe770 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 06:07:37 -0700 Subject: Removed a use of the Python ternary operator (for Python 2.4 support). --- pystache/renderer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pystache/renderer.py b/pystache/renderer.py index 86d349f..5bd2a3f 100644 --- a/pystache/renderer.py +++ b/pystache/renderer.py @@ -138,8 +138,11 @@ class Renderer(object): Convert a basestring to unicode, preserving any unicode subclass. """ - # Avoid the "double-decoding" TypeError. - return s if isinstance(s, unicode) else self.unicode(s) + # We type-check to avoid "TypeError: decoding Unicode is not supported". + # We avoid the Python ternary operator for Python 2.4 support. + if isinstance(s, unicode): + return s + return self.unicode(s) def _to_unicode_hard(self, s): """ -- cgit v1.2.1 From a0b4e4fe2d01a8871ccd371a00155b8f475de44b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 06:23:14 -0700 Subject: Eliminated one use of the with keyword for Python 2.4 support. --- pystache/loader.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pystache/loader.py b/pystache/loader.py index 3f049a4..bcba71b 100644 --- a/pystache/loader.py +++ b/pystache/loader.py @@ -5,8 +5,6 @@ This module provides a Loader class for locating and reading templates. """ -from __future__ import with_statement - import os import sys @@ -108,8 +106,12 @@ class Loader(object): Read the template at the given path, and return it as a unicode string. """ - with open(path, 'r') as f: + # We avoid use of the with keyword for Python 2.4 support. + f = open(path, 'r') + try: text = f.read() + finally: + f.close() if encoding is None: encoding = self.file_encoding -- cgit v1.2.1 From 1db59132e095e3d5a7cf33175c9e96f82b4c9901 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 06:30:03 -0700 Subject: Removed another use of the ternary operator for Python 2.4 support. --- pystache/parsed.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pystache/parsed.py b/pystache/parsed.py index 12b5cb0..5418ec1 100644 --- a/pystache/parsed.py +++ b/pystache/parsed.py @@ -37,7 +37,11 @@ class ParsedTemplate(object): Returns: a string of type unicode. """ - get_unicode = lambda val: val(context) if callable(val) else val + # We avoid use of the ternary operator for Python 2.4 support. + def get_unicode(val): + if callable(val): + return val(context) + return val parts = map(get_unicode, self._parse_tree) s = ''.join(parts) -- cgit v1.2.1 From b975e6aa1c68d37e812b0a723d4b6af919f42c61 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 07:14:06 -0700 Subject: Eliminated another use of the ternary operator for Python 2.4 support. --- pystache/spec_loader.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pystache/spec_loader.py b/pystache/spec_loader.py index ac706a5..3cb0f1a 100644 --- a/pystache/spec_loader.py +++ b/pystache/spec_loader.py @@ -36,12 +36,15 @@ class SpecLoader(object): """ if spec.template_rel_path is not None: return os.path.split(spec.template_rel_path) - # Otherwise, determine the file name separately. + locator = self.loader._make_locator() - template_name = (spec.template_name if spec.template_name is not None else - locator.make_template_name(spec)) + # We do not use the ternary operator for Python 2.4 support. + if spec.template_name is not None: + template_name = spec.template_name + else: + template_name = locator.make_template_name(spec) file_name = locator.make_file_name(template_name, spec.template_extension) -- cgit v1.2.1 From a8173aaa99e0f88b64290fc2713ee9d04cf983bb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 07:43:34 -0700 Subject: Now works with Python 2.5 (except for one unit test). --- pystache/commands.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pystache/commands.py b/pystache/commands.py index d6fb549..333e665 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -7,7 +7,13 @@ Run this script using the -h option for command-line help. """ -import json + +try: + import json +except: + # For Python 2.5 support: the json module is new in version 2.6. + import simplejson as json + # The optparse module is deprecated in Python 2.7 in favor of argparse. # However, argparse is not available in Python 2.6 and earlier. from optparse import OptionParser -- cgit v1.2.1 From acc31ffd19bb9d19cd85f717d897135805d43564 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 07:46:26 -0700 Subject: Fixed typo. --- tests/test_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_context.py b/tests/test_context.py index 49ebbd2..41b77b4 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -316,7 +316,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin): def test_get__default(self): """ - Test that get() respects the default value . + Test that get() respects the default value. """ context = Context() -- cgit v1.2.1 From 2e7942cf0652098b067da7eb16b9953754a85af4 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 19:23:05 -0700 Subject: Skip the test_built_in_type__integer() test when int.real isn't defined (Python 2.4 and 2.5). --- tests/test_context.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_context.py b/tests/test_context.py index 41b77b4..decf4fb 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -157,6 +157,17 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): item1 = MyInt(10) item2 = 10 + try: + item2.real + except AttributeError: + # Then skip this unit test. The numeric type hierarchy was + # added only in Python 2.6, in which case integers inherit + # from complex numbers the "real" attribute, etc: + # + # http://docs.python.org/library/numbers.html + # + return + self.assertEquals(item1.real, 10) self.assertEquals(item2.real, 10) -- cgit v1.2.1 From 09906e25d2df663db3e1d80fc676768e17630f96 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 22:58:59 -0700 Subject: Spec tests now work with Python 2.4: code now fully works with Python 2.4 --- pystache/commands.py | 3 ++- tests/test_spec.py | 60 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/pystache/commands.py b/pystache/commands.py index 333e665..1801d40 100644 --- a/pystache/commands.py +++ b/pystache/commands.py @@ -11,7 +11,8 @@ Run this script using the -h option for command-line help. try: import json except: - # For Python 2.5 support: the json module is new in version 2.6. + # The json module is new in Python 2.6, whereas simplejson is + # compatible with earlier versions. import simplejson as json # The optparse module is deprecated in Python 2.7 in favor of argparse. diff --git a/tests/test_spec.py b/tests/test_spec.py index a379f24..02f6080 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -3,32 +3,28 @@ """ Creates a unittest.TestCase for the tests defined in the mustache spec. -We did not call this file something like "test_spec.py" to avoid matching -nosetests's default regular expression "(?:^|[\b_\./-])[Tt]est". -This allows us to exclude the spec test cases by default when running -nosetests. To include the spec tests, one can use the following option, -for example-- +""" - nosetests -i spec +# TODO: this module can be cleaned up somewhat. -""" +try: + # We deserialize the json form rather than the yaml form because + # json libraries are available for Python 2.4. + import json +except: + # The json module is new in Python 2.6, whereas simplejson is + # compatible with earlier versions. + import simplejson as json import glob import os.path import unittest -import yaml from pystache.renderer import Renderer -def code_constructor(loader, node): - value = loader.construct_mapping(node) - return eval(value['python'], {}) - -yaml.add_constructor(u'!code', code_constructor) - -specs = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') -specs = glob.glob(os.path.join(specs, '*.yml')) +root_path = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs') +spec_paths = glob.glob(os.path.join(root_path, '*.json')) class MustacheSpec(unittest.TestCase): pass @@ -46,8 +42,16 @@ def buildTest(testData, spec_filename): expected = testData['expected'] data = testData['data'] + # Convert code strings to functions. + # TODO: make this section of code easier to understand. + new_data = {} + for key, val in data.iteritems(): + if isinstance(val, dict) and val.get('__tag__') == 'code': + val = eval(val['python']) + new_data[key] = val + renderer = Renderer(partials=partials) - actual = renderer.render(template, data) + actual = renderer.render(template, new_data) actual = actual.encode('utf-8') message = """%s @@ -64,14 +68,28 @@ def buildTest(testData, spec_filename): self.assertEquals(actual, expected, message) # The name must begin with "test" for nosetests test discovery to work. - test.__name__ = 'test: "%s"' % test_name + name = 'test: "%s"' % test_name + + # If we don't convert unicode to str, we get the following error: + # "TypeError: __name__ must be set to a string object" + test.__name__ = str(name) return test -for spec in specs: - file_name = os.path.basename(spec) +for spec_path in spec_paths: + + file_name = os.path.basename(spec_path) + + # We avoid use of the with keyword for Python 2.4 support. + f = open(spec_path, 'r') + try: + spec_data = json.load(f) + finally: + f.close() + + tests = spec_data['tests'] - for test in yaml.load(open(spec))['tests']: + for test in tests: test = buildTest(test, file_name) setattr(MustacheSpec, test.__name__, test) # Prevent this variable from being interpreted as another test. -- cgit v1.2.1 From 8a4cdc71be91db72442b4bd7561120b415bccc46 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 23:12:45 -0700 Subject: Added JSON requirements to README. --- README.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index c17ac63..632414d 100644 --- a/README.rst +++ b/README.rst @@ -23,16 +23,18 @@ Logo: `David Phillips`_ Requirements ============ -Pystache currently works using the following versions of Python: +Pystache currently works with the following versions of Python. JSON +support is needed only for the command-line interface and to run the +spec tests. -* Python 2.4 -* Python 2.5 +* Python 2.4 (requires ``simplejson`` version 2.0.9 or earlier) +* Python 2.5 (requires ``simplejson``) * Python 2.6 * Python 2.7 -TODO: mention simplejson for earlier versions of Python and yaml. - -TODO: try to replace yaml with json. +Python's simplejson_ is required for earlier versions of Python since +Python's json_ module is new in Python 2.6. Simplejson stopped officially +supporting Python 2.4 as of version 2.1.0. Install It @@ -95,10 +97,11 @@ The ``file_encoding`` attribute is the encoding the renderer uses to convert any files read from the file system to unicode. Similarly, ``string_encoding`` is the encoding the renderer uses to convert to unicode any other strings of type ``str`` encountered during the rendering process (e.g. context values -of type ``str``). The ``decode_errors`` attribute is what the renderer -passes as the ``errors`` argument to Python's `built-in unicode function`_ -``unicode()`` when converting. The valid values for this argument are -``strict``, ``ignore``, and ``replace``. +of type ``str``). + +The ``decode_errors`` attribute is what the renderer passes as the ``errors`` +argument to Python's `built-in unicode function`_ ``unicode()`` when converting. +The valid values for this argument are ``strict``, ``ignore``, and ``replace``. Each of these attributes can be set via the ``Renderer`` class's constructor using a keyword argument of the same name. In addition, the ``file_encoding`` @@ -163,6 +166,7 @@ Author .. _ctemplate: http://code.google.com/p/google-ctemplate/ .. _David Phillips: http://davidphillips.us/ .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html +.. _json: http://docs.python.org/library/json.html .. _Mustache: http://mustache.github.com/ .. _Mustache spec: https://github.com/mustache/spec .. _mustache(5): http://mustache.github.com/mustache.5.html @@ -170,5 +174,6 @@ Author .. _PyPI: http://pypi.python.org/pypi/pystache .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org +.. _simplejson: http://pypi.python.org/pypi/simplejson/ .. _built-in unicode function: http://docs.python.org/library/functions.html#unicode .. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 -- cgit v1.2.1 From da43d44b86591bedec027e05ddc318a935da6a0c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 23:24:02 -0700 Subject: Further README cleanups. --- README.rst | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 632414d..0036e08 100644 --- a/README.rst +++ b/README.rst @@ -23,18 +23,17 @@ Logo: `David Phillips`_ Requirements ============ -Pystache currently works with the following versions of Python. JSON -support is needed only for the command-line interface and to run the -spec tests. +Pystache currently works with the following versions of Python. -* Python 2.4 (requires ``simplejson`` version 2.0.9 or earlier) -* Python 2.5 (requires ``simplejson``) +* Python 2.4 (requires simplejson version 2.0.9 or earlier) +* Python 2.5 (requires simplejson) * Python 2.6 * Python 2.7 -Python's simplejson_ is required for earlier versions of Python since -Python's json_ module is new in Python 2.6. Simplejson stopped officially -supporting Python 2.4 as of version 2.1.0. +JSON support is needed only for the command-line interface and to run the +spec tests. Since Python's json_ module is new as of Python 2.6, earlier +versions of Python require simplejson_ to support JSON. Moreover, simplejson +stopped officially supporting Python 2.4 as of version 2.1.0. Install It @@ -94,20 +93,22 @@ Pystache converts ``str`` strings to unicode on input. These include the ``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes. The ``file_encoding`` attribute is the encoding the renderer uses to convert -any files read from the file system to unicode. Similarly, ``string_encoding`` +to unicode any files read from the file system. Similarly, ``string_encoding`` is the encoding the renderer uses to convert to unicode any other strings of type ``str`` encountered during the rendering process (e.g. context values of type ``str``). The ``decode_errors`` attribute is what the renderer passes as the ``errors`` -argument to Python's `built-in unicode function`_ ``unicode()`` when converting. -The valid values for this argument are ``strict``, ``ignore``, and ``replace``. +argument to Python's `built-in unicode function`_ ``unicode()`` when +converting. The valid values for this argument are ``strict``, ``ignore``, +and ``replace``. Each of these attributes can be set via the ``Renderer`` class's constructor -using a keyword argument of the same name. In addition, the ``file_encoding`` +using a keyword argument of the same name. See the Renderer class's +detailed docstrings for further details. In addition, the ``file_encoding`` attribute can be controlled on a per-view basis by subclassing the -``TemplateSpec`` class. When not specified explicitly, these attributes default -to values set in Pystache's ``defaults`` module. +``TemplateSpec`` class. When not specified explicitly, these attributes +default to values set in Pystache's ``defaults`` module. Test It -- cgit v1.2.1 From 599959265ca31b27e407724c192c8bf65cdd4704 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Mon, 2 Apr 2012 23:33:31 -0700 Subject: More README refinements. --- README.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 0036e08..70f0717 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ Logo: `David Phillips`_ Requirements ============ -Pystache currently works with the following versions of Python. +Pystache is tested with the following versions of Python: * Python 2.4 (requires simplejson version 2.0.9 or earlier) * Python 2.5 (requires simplejson) @@ -31,9 +31,10 @@ Pystache currently works with the following versions of Python. * Python 2.7 JSON support is needed only for the command-line interface and to run the -spec tests. Since Python's json_ module is new as of Python 2.6, earlier -versions of Python require simplejson_ to support JSON. Moreover, simplejson -stopped officially supporting Python 2.4 as of version 2.1.0. +spec tests. Python's json_ module is new as of Python 2.6. Python's +simplejson_ package works with earlier versions of Python. Because +simplejson stopped officially supporting Python 2.4 as of version 2.1.0, +Python 2.4 requires an earlier version. Install It @@ -105,7 +106,7 @@ and ``replace``. Each of these attributes can be set via the ``Renderer`` class's constructor using a keyword argument of the same name. See the Renderer class's -detailed docstrings for further details. In addition, the ``file_encoding`` +docstrings for further details. In addition, the ``file_encoding`` attribute can be controlled on a per-view basis by subclassing the ``TemplateSpec`` class. When not specified explicitly, these attributes default to values set in Pystache's ``defaults`` module. -- cgit v1.2.1 From 9aca794c1be78d8d6b453a42e29c2e474ab75043 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 3 Apr 2012 08:21:51 -0700 Subject: Added to README a link to the Python documentation's "Tips for Writing Unicode-aware Programs." --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 70f0717..9781fc2 100644 --- a/README.rst +++ b/README.rst @@ -85,7 +85,7 @@ Unicode Handling This section describes Pystache's handling of unicode (e.g. strings and encodings). -Internally, Pystache uses only unicode strings. For input, Pystache accepts +Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts both ``unicode`` and ``str`` strings. For output, Pystache's template rendering methods return only unicode. @@ -173,6 +173,7 @@ Author .. _Mustache spec: https://github.com/mustache/spec .. _mustache(5): http://mustache.github.com/mustache.5.html .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html +.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs .. _PyPI: http://pypi.python.org/pypi/pystache .. _Pystache: https://github.com/defunkt/pystache .. _semantically versioned: http://semver.org -- cgit v1.2.1 From ada8ffb0217b2ff8ecf7951252781598beae9edd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 3 Apr 2012 17:52:57 -0700 Subject: Cleaned up the history file prior to merging to master. --- HISTORY.rst | 53 +++++++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7c5ae4a..21b862f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,14 +1,16 @@ History ======= -0.5.0 (TBD) ------------ +0.5.0 (2012-04-03) +------------------ This version represents a major rewrite and refactoring of the code base that also adds features and fixes many bugs. All functionality and nearly all unit tests have been preserved. However, some backwards incompatible changes to the API have been made. +Below is a selection of some of the changes (not exhaustive). + Highlights: * Pystache now passes all tests in version 1.0.3 of the `Mustache spec`_. [pvande] @@ -21,43 +23,30 @@ Highlights: * Introduced separation of concerns and removed circular dependencies (e.g. between Template and View classes, cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. -* Expanded test coverage: nosetests now includes doctests and ~105 test cases - from the Mustache spec (for a total of ~315 unit tests up from 56). -* Added rudimentary benchmarking script. - -TODO: complete the list of key changes. - -Features: +* Expanded test coverage: nosetests now runs doctests and ~105 test cases + from the Mustache spec (increasing test count from 56 to ~315). +* Added a rudimentary benchmarking script to gauge performance while refactoring. +* Extensive documentation added (e.g. docstrings). -* Views and Renderers accept a custom template loader. Also, this loader - can be a dictionary of partials. [cjerdonek] -* Added a command-line interface. [vrde, cjerdonek] -* Custom escape function can now be passed to Template constructor. [cjerdonek] -* Template class can now handle non-ascii characters in non-unicode strings. - Added default_encoding and decode_errors to Template constructor arguments. - [cjerdonek] -* Loader supports a decode_errors argument. [cjerdonek] +Other changes: -API changes: - -* Removed output_encoding options. [cjerdonek] -* Removed automatic use of markupsafe, if available. [cjerdonek] +* Added a command-line interface. [vrde] +* Renderer now accepts a custom partial loader (e.g. a dictionary) and custom + escape function. +* Rendering now supports non-ascii characters in str strings. +* Added string encoding, file encoding, and errors options for decoding to unicode. +* Removed the output encoding option. +* Removed the use of markupsafe. Bug fixes: * Context values no longer processed as template strings. [jakearchibald] -* Passing ``**kwargs`` to ``Template()`` modified the context. [cjerdonek] -* Passing ``**kwargs`` to ``Template()`` with no context raised an exception. [cjerdonek] -* Whitespace surrounding sections is no longer altered, in accordance with - the mustache spec. [heliodor] -* Fixed an issue that affected the rendering of zeroes when using certain - implementations of Python (i.e. PyPy). [alex] -* Extensionless template files could not be loaded. [cjerdonek] +* Whitespace surrounding sections is no longer altered, per the spec. [heliodor] +* Zeroes now render correctly when using PyPy. [alex] * Multline comments now permitted. [fczuardi] - -Misc: - -* Added some docstrings. [kennethreitz] +* Extensionless template files are now supported. +* Passing ``**kwargs`` to ``Template()`` no longer modifies the context. +* Passing ``**kwargs`` to ``Template()`` with no context no longer raises an exception. 0.4.1 (2012-03-25) ------------------ -- cgit v1.2.1 From 368f0dfd2f61b0e4a92d530e033eaec4a6fcfeb9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 3 Apr 2012 18:40:16 -0700 Subject: A couple tweaks to the HISTORY file. --- HISTORY.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 21b862f..dcf8a99 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,16 +24,16 @@ Highlights: between Template and View classes, cf. `issue #13`_). * Unicode now used consistently throughout the rendering process. * Expanded test coverage: nosetests now runs doctests and ~105 test cases - from the Mustache spec (increasing test count from 56 to ~315). + from the Mustache spec (increasing the number of tests from 56 to ~315). * Added a rudimentary benchmarking script to gauge performance while refactoring. * Extensive documentation added (e.g. docstrings). Other changes: * Added a command-line interface. [vrde] -* Renderer now accepts a custom partial loader (e.g. a dictionary) and custom - escape function. -* Rendering now supports non-ascii characters in str strings. +* The main rendering class now accepts a custom partial loader (e.g. a dictionary) + and a custom escape function. +* Non-ascii characters in str strings are now supported while rendering. * Added string encoding, file encoding, and errors options for decoding to unicode. * Removed the output encoding option. * Removed the use of markupsafe. -- cgit v1.2.1