changeset 454:96d363e2da4b

templating: Let Jinja2 cache the parsed template for page contents.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 06 Jul 2015 21:32:40 -0700
parents 8351a77e13f5
children cb3446be44b7
files piecrust/page.py piecrust/rendering.py piecrust/templating/base.py piecrust/templating/jinjaengine.py piecrust/templating/pystacheengine.py
diffstat 5 files changed, 72 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/page.py	Tue Jul 07 20:19:54 2015 -0700
+++ b/piecrust/page.py	Mon Jul 06 21:32:40 2015 -0700
@@ -199,19 +199,18 @@
 class ContentSegment(object):
     debug_render_func = 'debug_render'
 
-    def __init__(self, content=None, fmt=None):
+    def __init__(self):
         self.parts = []
-        if content is not None:
-            self.parts.append(ContentSegmentPart(content, fmt))
 
     def debug_render(self):
         return '\n'.join([p.content for p in self.parts])
 
 
 class ContentSegmentPart(object):
-    def __init__(self, content, fmt=None, line=-1):
+    def __init__(self, content, fmt=None, offset=-1, line=-1):
         self.content = content
         self.fmt = fmt
+        self.offset = offset
         self.line = line
 
     def __str__(self):
@@ -223,7 +222,8 @@
     for key, seg_data in data.items():
         seg = ContentSegment()
         for p_data in seg_data:
-            part = ContentSegmentPart(p_data['c'], p_data['f'], p_data['l'])
+            part = ContentSegmentPart(p_data['c'], p_data['f'], p_data['o'],
+                                      p_data['l'])
             seg.parts.append(part)
         segments[key] = seg
     return segments
@@ -234,7 +234,8 @@
     for key, seg in segments.items():
         seg_data = []
         for part in seg.parts:
-            p_data = {'c': part.content, 'f': part.fmt, 'l': part.line}
+            p_data = {'c': part.content, 'f': part.fmt, 'o': part.offset,
+                      'l': part.line}
             seg_data.append(p_data)
         data[key] = seg_data
     return data
@@ -357,7 +358,8 @@
         # First part, before the first format change.
         part_text = raw[start:matches[0].start()]
         parts.append(
-                ContentSegmentPart(part_text, first_part_fmt, line_offset))
+                ContentSegmentPart(part_text, first_part_fmt, start,
+                                   line_offset))
         line_offset += _count_lines(part_text)
 
         for i in range(1, num_matches):
@@ -366,17 +368,20 @@
             part_text = raw[m1.end() + 1:m2.start()]
             parts.append(
                     ContentSegmentPart(
-                        part_text, m1.group('fmt'), line_offset))
+                        part_text, m1.group('fmt'), m1.end() + 1,
+                        line_offset))
             line_offset += _count_lines(part_text)
 
         lastm = matches[-1]
         part_text = raw[lastm.end() + 1:end]
         parts.append(ContentSegmentPart(
-                part_text, lastm.group('fmt'), line_offset))
+                part_text, lastm.group('fmt'), lastm.end() + 1,
+                line_offset))
 
         return parts, line_offset
     else:
         part_text = raw[start:end]
-        parts = [ContentSegmentPart(part_text, first_part_fmt, line_offset)]
+        parts = [ContentSegmentPart(part_text, first_part_fmt, start,
+                                    line_offset)]
         return parts, line_offset
 
--- a/piecrust/rendering.py	Tue Jul 07 20:19:54 2015 -0700
+++ b/piecrust/rendering.py	Mon Jul 06 21:32:40 2015 -0700
@@ -310,9 +310,8 @@
             part_format = seg_part.fmt or format_name
             try:
                 with app.env.timerScope(engine.__class__.__name__):
-                    part_text = engine.renderString(
-                            seg_part.content, page_data,
-                            filename=page.path)
+                    part_text = engine.renderSegmentPart(
+                            page.path, seg_part, page_data)
             except TemplatingError as err:
                 err.lineno += seg_part.line
                 raise err
--- a/piecrust/templating/base.py	Tue Jul 07 20:19:54 2015 -0700
+++ b/piecrust/templating/base.py	Mon Jul 06 21:32:40 2015 -0700
@@ -27,7 +27,7 @@
     def initialize(self, app):
         self.app = app
 
-    def renderString(self, txt, data, filename=None, line_offset=0):
+    def renderSegmentPart(self, path, seg_part, data):
         raise NotImplementedError()
 
     def renderFile(self, paths, data):
--- a/piecrust/templating/jinjaengine.py	Tue Jul 07 20:19:54 2015 -0700
+++ b/piecrust/templating/jinjaengine.py	Mon Jul 06 21:32:40 2015 -0700
@@ -33,25 +33,19 @@
     def __init__(self):
         self.env = None
 
-    def renderString(self, txt, data, filename=None):
+    def renderSegmentPart(self, path, seg_part, data):
         self._ensureLoaded()
 
-        do_render = False
-        index = txt.find('{')
-        while index >= 0:
-            ch = txt[index + 1]
-            if ch == '{' or ch == '%':
-                do_render = True
-                break
-            index = txt.find('{', index + 1)
+        if not _string_needs_render(seg_part.content):
+            return seg_part.content
 
-        if not do_render:
-            return txt
-
+        part_path = _make_segment_part_path(path, seg_part.offset)
+        self.env.loader.segment_parts_cache[part_path] = (
+                path, seg_part.content)
         try:
-            tpl = self.env.from_string(txt)
+            tpl = self.env.get_template(part_path)
         except TemplateSyntaxError as tse:
-            raise self._getTemplatingError(tse, filename=filename)
+            raise self._getTemplatingError(tse, filename=path)
         except TemplateNotFound:
             raise TemplateNotFoundError()
 
@@ -99,8 +93,8 @@
             autoescape = True
 
         logger.debug("Creating Jinja environment with folders: %s" %
-                self.app.templates_dirs)
-        loader = FileSystemLoader(self.app.templates_dirs)
+                     self.app.templates_dirs)
+        loader = PieCrustLoader(self.app.templates_dirs)
         extensions = [
                 PieCrustHighlightExtension,
                 PieCrustCacheExtension,
@@ -114,6 +108,42 @@
                 extensions=extensions)
 
 
+def _string_needs_render(txt):
+    index = txt.find('{')
+    while index >= 0:
+        ch = txt[index + 1]
+        if ch == '{' or ch == '%':
+            return True
+        index = txt.find('{', index + 1)
+    return False
+
+
+def _make_segment_part_path(path, start):
+    return '$part=%s:%d' % (path, start)
+
+
+class PieCrustLoader(FileSystemLoader):
+    def __init__(self, searchpath, encoding='utf-8'):
+        super(PieCrustLoader, self).__init__(searchpath, encoding)
+        self.segment_parts_cache = {}
+
+    def get_source(self, environment, template):
+        if template.startswith('$part='):
+            filename, seg_part = self.segment_parts_cache[template]
+
+            mtime = os.path.getmtime(filename)
+
+            def uptodate():
+                try:
+                    return os.path.getmtime(filename) == mtime
+                except OSError:
+                    return False
+
+            return seg_part, filename, uptodate
+
+        return super(PieCrustLoader, self).get_source(environment, template)
+
+
 class PieCrustEnvironment(Environment):
     def __init__(self, app, *args, **kwargs):
         self.app = app
@@ -313,7 +343,8 @@
         args = [parser.parse_expression()]
 
         # Extract optional arguments.
-        kwarg_names = {'line_numbers': 0, 'use_classes': 0, 'class': 1, 'id': 1}
+        kwarg_names = {'line_numbers': 0, 'use_classes': 0, 'class': 1,
+                       'id': 1}
         kwargs = {}
         while not parser.stream.current.test('block_end'):
             name = parser.stream.expect('name')
@@ -333,7 +364,7 @@
                          [], [], body).set_lineno(lineno)
 
     def _highlight(self, lang, line_numbers=False, use_classes=False,
-            css_class=None, css_id=None, caller=None):
+                   css_class=None, css_id=None, caller=None):
         # Try to be mostly compatible with Jinja2-highlight's settings.
         body = caller()
 
@@ -387,12 +418,12 @@
         # now we parse the body of the cache block up to `endpccache` and
         # drop the needle (which would always be `endpccache` in that case)
         body = parser.parse_statements(['name:endpccache', 'name:endcache'],
-                drop_needle=True)
+                                       drop_needle=True)
 
         # now return a `CallBlock` node that calls our _cache_support
         # helper method on this extension.
         return CallBlock(self.call_method('_cache_support', args),
-                               [], [], body).set_lineno(lineno)
+                         [], [], body).set_lineno(lineno)
 
     def _cache_support(self, name, caller):
         key = self.environment.piecrust_cache_prefix + name
--- a/piecrust/templating/pystacheengine.py	Tue Jul 07 20:19:54 2015 -0700
+++ b/piecrust/templating/pystacheengine.py	Mon Jul 06 21:32:40 2015 -0700
@@ -16,14 +16,14 @@
     def __init__(self):
         self.renderer = None
 
-    def renderString(self, txt, data, filename=None):
+    def renderSegmentPart(self, path, seg_part, data):
         self._ensureLoaded()
         try:
-            return self.renderer.render(txt, data)
+            return self.renderer.render(seg_part.content, data)
         except pystache.common.TemplateNotFoundError as ex:
             raise TemplateNotFoundError() from ex
         except pystache.common.PystacheError as ex:
-            raise TemplatingError(str(ex), filename) from ex
+            raise TemplatingError(str(ex), path) from ex
 
     def renderFile(self, paths, data):
         self._ensureLoaded()