diff piecrust/rendering.py @ 852:4850f8c21b6e

core: Start of the big refactor for PieCrust 3.0. * Everything is a `ContentSource`, including assets directories. * Most content sources are subclasses of the base file-system source. * A source is processed by a "pipeline", and there are 2 built-in pipelines, one for assets and one for pages. The asset pipeline is vaguely functional, but the page pipeline is completely broken right now. * Rewrite the baking process as just running appropriate pipelines on each content item. This should allow for better parallelization.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 17 May 2017 00:11:48 -0700
parents dca51cd8147a
children f070a4fc033c
line wrap: on
line diff
--- a/piecrust/rendering.py	Sat Apr 29 21:42:22 2017 -0700
+++ b/piecrust/rendering.py	Wed May 17 00:11:48 2017 -0700
@@ -2,13 +2,10 @@
 import os.path
 import copy
 import logging
-from werkzeug.utils import cached_property
 from piecrust.data.builder import (
-        DataBuildingContext, build_page_data, build_layout_data)
-from piecrust.data.filters import (
-        PaginationFilter, SettingFilterClause, page_value_accessor)
+    DataBuildingContext, build_page_data, add_layout_data)
 from piecrust.fastpickle import _pickle_object, _unpickle_object
-from piecrust.sources.base import PageSource
+from piecrust.sources.base import ContentSource
 from piecrust.templating.base import TemplateNotFoundError, TemplatingError
 
 
@@ -19,7 +16,7 @@
                                  re.MULTILINE)
 
 
-class PageRenderingError(Exception):
+class RenderingError(Exception):
     pass
 
 
@@ -27,19 +24,6 @@
     pass
 
 
-class QualifiedPage(object):
-    def __init__(self, page, route, route_metadata):
-        self.page = page
-        self.route = route
-        self.route_metadata = route_metadata
-
-    def getUri(self, sub_num=1):
-        return self.route.getUri(self.route_metadata, sub_num=sub_num)
-
-    def __getattr__(self, name):
-        return getattr(self.page, name)
-
-
 class RenderedSegments(object):
     def __init__(self, segments, render_pass_info):
         self.segments = segments
@@ -53,17 +37,15 @@
 
 
 class RenderedPage(object):
-    def __init__(self, page, uri, num=1):
-        self.page = page
-        self.uri = uri
-        self.num = num
+    def __init__(self, qualified_page):
+        self.qualified_page = qualified_page
         self.data = None
         self.content = None
         self.render_info = [None, None]
 
     @property
     def app(self):
-        return self.page.app
+        return self.qualified_page.app
 
     def copyRenderInfo(self):
         return copy.deepcopy(self.render_info)
@@ -94,13 +76,10 @@
         return self._custom_info.get(key, default)
 
 
-class PageRenderingContext(object):
-    def __init__(self, qualified_page, page_num=1,
-                 force_render=False, is_from_request=False):
-        self.page = qualified_page
-        self.page_num = page_num
+class RenderingContext(object):
+    def __init__(self, qualified_page, force_render=False):
+        self.qualified_page = qualified_page
         self.force_render = force_render
-        self.is_from_request = is_from_request
         self.pagination_source = None
         self.pagination_filter = None
         self.custom_data = {}
@@ -109,15 +88,7 @@
 
     @property
     def app(self):
-        return self.page.app
-
-    @property
-    def source_metadata(self):
-        return self.page.source_metadata
-
-    @cached_property
-    def uri(self):
-        return self.page.getUri(self.page_num)
+        return self.qualified_page.app
 
     @property
     def current_pass_info(self):
@@ -142,7 +113,7 @@
 
     def addUsedSource(self, source):
         self._raiseIfNoCurrentPass()
-        if isinstance(source, PageSource):
+        if isinstance(source, ContentSource):
             pass_info = self.current_pass_info
             pass_info.used_source_names.add(source.name)
 
@@ -151,103 +122,149 @@
             raise Exception("No rendering pass is currently active.")
 
 
+class RenderingContextStack(object):
+    def __init__(self):
+        self._ctx_stack = []
+
+    @property
+    def current_ctx(self):
+        if len(self._ctx_stack) == 0:
+            return None
+        return self._ctx_stack[-1]
+
+    @property
+    def is_main_ctx(self):
+        return len(self._ctx_stack) == 1
+
+    def hasPage(self, page):
+        for ei in self._ctx_stack:
+            if ei.qualified_page.page == page:
+                return True
+        return False
+
+    def pushCtx(self, render_ctx):
+        for ctx in self._ctx_stack:
+            if ctx.qualified_page.page == render_ctx.qualified_page.page:
+                raise Exception("Loop detected during rendering!")
+        self._ctx_stack.append(render_ctx)
+
+    def popCtx(self):
+        del self._ctx_stack[-1]
+
+    def clear(self):
+        self._ctx_stack = []
+
+
 def render_page(ctx):
-    eis = ctx.app.env.exec_info_stack
-    eis.pushPage(ctx.page, ctx)
+    env = ctx.app.env
+
+    stack = env.render_ctx_stack
+    stack.pushCtx(ctx)
+
+    qpage = ctx.qualified_page
+
     try:
         # Build the data for both segment and layout rendering.
-        with ctx.app.env.timerScope("BuildRenderData"):
+        with env.timerScope("BuildRenderData"):
             page_data = _build_render_data(ctx)
 
         # Render content segments.
         ctx.setCurrentPass(PASS_FORMATTING)
-        repo = ctx.app.env.rendered_segments_repository
+        repo = env.rendered_segments_repository
         save_to_fs = True
-        if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page:
+        if env.fs_cache_only_for_main_page and not stack.is_main_ctx:
             save_to_fs = False
-        with ctx.app.env.timerScope("PageRenderSegments"):
-            if repo and not ctx.force_render:
+        with env.timerScope("PageRenderSegments"):
+            if repo is not None and not ctx.force_render:
                 render_result = repo.get(
-                        ctx.uri,
-                        lambda: _do_render_page_segments(ctx.page, page_data),
-                        fs_cache_time=ctx.page.path_mtime,
-                        save_to_fs=save_to_fs)
+                    qpage.uri,
+                    lambda: _do_render_page_segments(ctx, page_data),
+                    fs_cache_time=qpage.page.content_mtime,
+                    save_to_fs=save_to_fs)
             else:
-                render_result = _do_render_page_segments(ctx.page, page_data)
+                render_result = _do_render_page_segments(ctx, page_data)
                 if repo:
-                    repo.put(ctx.uri, render_result, save_to_fs)
+                    repo.put(qpage.uri, render_result, save_to_fs)
 
         # Render layout.
-        page = ctx.page
         ctx.setCurrentPass(PASS_RENDERING)
-        layout_name = page.config.get('layout')
+        layout_name = qpage.page.config.get('layout')
         if layout_name is None:
-            layout_name = page.source.config.get('default_layout', 'default')
+            layout_name = qpage.page.source.config.get(
+                'default_layout', 'default')
         null_names = ['', 'none', 'nil']
         if layout_name not in null_names:
             with ctx.app.env.timerScope("BuildRenderData"):
-                build_layout_data(page, page_data, render_result['segments'])
+                add_layout_data(page_data, render_result['segments'])
 
             with ctx.app.env.timerScope("PageRenderLayout"):
-                layout_result = _do_render_layout(layout_name, page, page_data)
+                layout_result = _do_render_layout(
+                    layout_name, qpage, page_data)
         else:
             layout_result = {
-                    'content': render_result['segments']['content'],
-                    'pass_info': None}
+                'content': render_result['segments']['content'],
+                'pass_info': None}
 
-        rp = RenderedPage(page, ctx.uri, ctx.page_num)
+        rp = RenderedPage(qpage)
         rp.data = page_data
         rp.content = layout_result['content']
         rp.render_info[PASS_FORMATTING] = _unpickle_object(
-                render_result['pass_info'])
+            render_result['pass_info'])
         if layout_result['pass_info'] is not None:
             rp.render_info[PASS_RENDERING] = _unpickle_object(
-                    layout_result['pass_info'])
+                layout_result['pass_info'])
         return rp
+
     except Exception as ex:
         if ctx.app.debug:
             raise
         logger.exception(ex)
         page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir)
         raise Exception("Error rendering page: %s" % page_rel_path) from ex
+
     finally:
         ctx.setCurrentPass(PASS_NONE)
-        eis.popPage()
+        stack.popCtx()
 
 
 def render_page_segments(ctx):
-    eis = ctx.app.env.exec_info_stack
-    eis.pushPage(ctx.page, ctx)
+    env = ctx.app.env
+
+    stack = env.render_ctx_stack
+    stack.pushCtx(ctx)
+
+    qpage = ctx.qualified_page
+
     try:
         ctx.setCurrentPass(PASS_FORMATTING)
         repo = ctx.app.env.rendered_segments_repository
         save_to_fs = True
-        if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page:
+        if ctx.app.env.fs_cache_only_for_main_page and not stack.is_main_ctx:
             save_to_fs = False
         with ctx.app.env.timerScope("PageRenderSegments"):
-            if repo and not ctx.force_render:
+            if repo is not None and not ctx.force_render:
                 render_result = repo.get(
-                    ctx.uri,
+                    qpage.uri,
                     lambda: _do_render_page_segments_from_ctx(ctx),
-                    fs_cache_time=ctx.page.path_mtime,
+                    fs_cache_time=qpage.page.content_mtime,
                     save_to_fs=save_to_fs)
             else:
                 render_result = _do_render_page_segments_from_ctx(ctx)
                 if repo:
-                    repo.put(ctx.uri, render_result, save_to_fs)
+                    repo.put(qpage.uri, render_result, save_to_fs)
     finally:
         ctx.setCurrentPass(PASS_NONE)
-        eis.popPage()
+        stack.popCtx()
 
     rs = RenderedSegments(
-            render_result['segments'],
-            _unpickle_object(render_result['pass_info']))
+        render_result['segments'],
+        _unpickle_object(render_result['pass_info']))
     return rs
 
 
 def _build_render_data(ctx):
     with ctx.app.env.timerScope("PageDataBuild"):
-        data_ctx = DataBuildingContext(ctx.page, page_num=ctx.page_num)
+        data_ctx = DataBuildingContext(ctx.qualified_page)
         data_ctx.pagination_source = ctx.pagination_source
         data_ctx.pagination_filter = ctx.pagination_filter
         page_data = build_page_data(data_ctx)
@@ -258,16 +275,13 @@
 
 def _do_render_page_segments_from_ctx(ctx):
     page_data = _build_render_data(ctx)
-    return _do_render_page_segments(ctx.page, page_data)
+    return _do_render_page_segments(ctx, page_data)
 
 
-def _do_render_page_segments(page, page_data):
+def _do_render_page_segments(ctx, page_data):
+    page = ctx.qualified_page.page
     app = page.app
 
-    cpi = app.env.exec_info_stack.current_page_info
-    assert cpi is not None
-    assert cpi.page == page
-
     engine_name = page.config.get('template_engine')
     format_name = page.config.get('format')
 
@@ -282,7 +296,7 @@
                 with app.env.timerScope(
                         engine.__class__.__name__ + '_segment'):
                     part_text = engine.renderSegmentPart(
-                            page.path, seg_part, page_data)
+                        page.path, seg_part, page_data)
             except TemplatingError as err:
                 err.lineno += seg_part.line
                 raise err
@@ -298,10 +312,10 @@
                 content_abstract = seg_text[:offset]
                 formatted_segments['content.abstract'] = content_abstract
 
-    pass_info = cpi.render_ctx.render_passes[PASS_FORMATTING]
+    pass_info = ctx.render_passes[PASS_FORMATTING]
     res = {
-            'segments': formatted_segments,
-            'pass_info': _pickle_object(pass_info)}
+        'segments': formatted_segments,
+        'pass_info': _pickle_object(pass_info)}
     return res