comparison 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
comparison
equal deleted inserted replaced
851:2c7e57d80bba 852:4850f8c21b6e
1 import re 1 import re
2 import os.path 2 import os.path
3 import copy 3 import copy
4 import logging 4 import logging
5 from werkzeug.utils import cached_property
6 from piecrust.data.builder import ( 5 from piecrust.data.builder import (
7 DataBuildingContext, build_page_data, build_layout_data) 6 DataBuildingContext, build_page_data, add_layout_data)
8 from piecrust.data.filters import (
9 PaginationFilter, SettingFilterClause, page_value_accessor)
10 from piecrust.fastpickle import _pickle_object, _unpickle_object 7 from piecrust.fastpickle import _pickle_object, _unpickle_object
11 from piecrust.sources.base import PageSource 8 from piecrust.sources.base import ContentSource
12 from piecrust.templating.base import TemplateNotFoundError, TemplatingError 9 from piecrust.templating.base import TemplateNotFoundError, TemplatingError
13 10
14 11
15 logger = logging.getLogger(__name__) 12 logger = logging.getLogger(__name__)
16 13
17 14
18 content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$', 15 content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$',
19 re.MULTILINE) 16 re.MULTILINE)
20 17
21 18
22 class PageRenderingError(Exception): 19 class RenderingError(Exception):
23 pass 20 pass
24 21
25 22
26 class TemplateEngineNotFound(Exception): 23 class TemplateEngineNotFound(Exception):
27 pass 24 pass
28
29
30 class QualifiedPage(object):
31 def __init__(self, page, route, route_metadata):
32 self.page = page
33 self.route = route
34 self.route_metadata = route_metadata
35
36 def getUri(self, sub_num=1):
37 return self.route.getUri(self.route_metadata, sub_num=sub_num)
38
39 def __getattr__(self, name):
40 return getattr(self.page, name)
41 25
42 26
43 class RenderedSegments(object): 27 class RenderedSegments(object):
44 def __init__(self, segments, render_pass_info): 28 def __init__(self, segments, render_pass_info):
45 self.segments = segments 29 self.segments = segments
51 self.content = content 35 self.content = content
52 self.render_pass_info = render_pass_info 36 self.render_pass_info = render_pass_info
53 37
54 38
55 class RenderedPage(object): 39 class RenderedPage(object):
56 def __init__(self, page, uri, num=1): 40 def __init__(self, qualified_page):
57 self.page = page 41 self.qualified_page = qualified_page
58 self.uri = uri
59 self.num = num
60 self.data = None 42 self.data = None
61 self.content = None 43 self.content = None
62 self.render_info = [None, None] 44 self.render_info = [None, None]
63 45
64 @property 46 @property
65 def app(self): 47 def app(self):
66 return self.page.app 48 return self.qualified_page.app
67 49
68 def copyRenderInfo(self): 50 def copyRenderInfo(self):
69 return copy.deepcopy(self.render_info) 51 return copy.deepcopy(self.render_info)
70 52
71 53
92 if create_if_missing: 74 if create_if_missing:
93 return self._custom_info.setdefault(key, default) 75 return self._custom_info.setdefault(key, default)
94 return self._custom_info.get(key, default) 76 return self._custom_info.get(key, default)
95 77
96 78
97 class PageRenderingContext(object): 79 class RenderingContext(object):
98 def __init__(self, qualified_page, page_num=1, 80 def __init__(self, qualified_page, force_render=False):
99 force_render=False, is_from_request=False): 81 self.qualified_page = qualified_page
100 self.page = qualified_page
101 self.page_num = page_num
102 self.force_render = force_render 82 self.force_render = force_render
103 self.is_from_request = is_from_request
104 self.pagination_source = None 83 self.pagination_source = None
105 self.pagination_filter = None 84 self.pagination_filter = None
106 self.custom_data = {} 85 self.custom_data = {}
107 self.render_passes = [None, None] # Same length as RENDER_PASSES 86 self.render_passes = [None, None] # Same length as RENDER_PASSES
108 self._current_pass = PASS_NONE 87 self._current_pass = PASS_NONE
109 88
110 @property 89 @property
111 def app(self): 90 def app(self):
112 return self.page.app 91 return self.qualified_page.app
113
114 @property
115 def source_metadata(self):
116 return self.page.source_metadata
117
118 @cached_property
119 def uri(self):
120 return self.page.getUri(self.page_num)
121 92
122 @property 93 @property
123 def current_pass_info(self): 94 def current_pass_info(self):
124 if self._current_pass != PASS_NONE: 95 if self._current_pass != PASS_NONE:
125 return self.render_passes[self._current_pass] 96 return self.render_passes[self._current_pass]
140 pass_info.pagination_has_more = paginator.has_more 111 pass_info.pagination_has_more = paginator.has_more
141 self.addUsedSource(paginator._source) 112 self.addUsedSource(paginator._source)
142 113
143 def addUsedSource(self, source): 114 def addUsedSource(self, source):
144 self._raiseIfNoCurrentPass() 115 self._raiseIfNoCurrentPass()
145 if isinstance(source, PageSource): 116 if isinstance(source, ContentSource):
146 pass_info = self.current_pass_info 117 pass_info = self.current_pass_info
147 pass_info.used_source_names.add(source.name) 118 pass_info.used_source_names.add(source.name)
148 119
149 def _raiseIfNoCurrentPass(self): 120 def _raiseIfNoCurrentPass(self):
150 if self._current_pass == PASS_NONE: 121 if self._current_pass == PASS_NONE:
151 raise Exception("No rendering pass is currently active.") 122 raise Exception("No rendering pass is currently active.")
152 123
153 124
125 class RenderingContextStack(object):
126 def __init__(self):
127 self._ctx_stack = []
128
129 @property
130 def current_ctx(self):
131 if len(self._ctx_stack) == 0:
132 return None
133 return self._ctx_stack[-1]
134
135 @property
136 def is_main_ctx(self):
137 return len(self._ctx_stack) == 1
138
139 def hasPage(self, page):
140 for ei in self._ctx_stack:
141 if ei.qualified_page.page == page:
142 return True
143 return False
144
145 def pushCtx(self, render_ctx):
146 for ctx in self._ctx_stack:
147 if ctx.qualified_page.page == render_ctx.qualified_page.page:
148 raise Exception("Loop detected during rendering!")
149 self._ctx_stack.append(render_ctx)
150
151 def popCtx(self):
152 del self._ctx_stack[-1]
153
154 def clear(self):
155 self._ctx_stack = []
156
157
154 def render_page(ctx): 158 def render_page(ctx):
155 eis = ctx.app.env.exec_info_stack 159 env = ctx.app.env
156 eis.pushPage(ctx.page, ctx) 160
161 stack = env.render_ctx_stack
162 stack.pushCtx(ctx)
163
164 qpage = ctx.qualified_page
165
157 try: 166 try:
158 # Build the data for both segment and layout rendering. 167 # Build the data for both segment and layout rendering.
159 with ctx.app.env.timerScope("BuildRenderData"): 168 with env.timerScope("BuildRenderData"):
160 page_data = _build_render_data(ctx) 169 page_data = _build_render_data(ctx)
161 170
162 # Render content segments. 171 # Render content segments.
163 ctx.setCurrentPass(PASS_FORMATTING) 172 ctx.setCurrentPass(PASS_FORMATTING)
164 repo = ctx.app.env.rendered_segments_repository 173 repo = env.rendered_segments_repository
165 save_to_fs = True 174 save_to_fs = True
166 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: 175 if env.fs_cache_only_for_main_page and not stack.is_main_ctx:
167 save_to_fs = False 176 save_to_fs = False
168 with ctx.app.env.timerScope("PageRenderSegments"): 177 with env.timerScope("PageRenderSegments"):
169 if repo and not ctx.force_render: 178 if repo is not None and not ctx.force_render:
170 render_result = repo.get( 179 render_result = repo.get(
171 ctx.uri, 180 qpage.uri,
172 lambda: _do_render_page_segments(ctx.page, page_data), 181 lambda: _do_render_page_segments(ctx, page_data),
173 fs_cache_time=ctx.page.path_mtime, 182 fs_cache_time=qpage.page.content_mtime,
174 save_to_fs=save_to_fs) 183 save_to_fs=save_to_fs)
175 else: 184 else:
176 render_result = _do_render_page_segments(ctx.page, page_data) 185 render_result = _do_render_page_segments(ctx, page_data)
177 if repo: 186 if repo:
178 repo.put(ctx.uri, render_result, save_to_fs) 187 repo.put(qpage.uri, render_result, save_to_fs)
179 188
180 # Render layout. 189 # Render layout.
181 page = ctx.page
182 ctx.setCurrentPass(PASS_RENDERING) 190 ctx.setCurrentPass(PASS_RENDERING)
183 layout_name = page.config.get('layout') 191 layout_name = qpage.page.config.get('layout')
184 if layout_name is None: 192 if layout_name is None:
185 layout_name = page.source.config.get('default_layout', 'default') 193 layout_name = qpage.page.source.config.get(
194 'default_layout', 'default')
186 null_names = ['', 'none', 'nil'] 195 null_names = ['', 'none', 'nil']
187 if layout_name not in null_names: 196 if layout_name not in null_names:
188 with ctx.app.env.timerScope("BuildRenderData"): 197 with ctx.app.env.timerScope("BuildRenderData"):
189 build_layout_data(page, page_data, render_result['segments']) 198 add_layout_data(page_data, render_result['segments'])
190 199
191 with ctx.app.env.timerScope("PageRenderLayout"): 200 with ctx.app.env.timerScope("PageRenderLayout"):
192 layout_result = _do_render_layout(layout_name, page, page_data) 201 layout_result = _do_render_layout(
202 layout_name, qpage, page_data)
193 else: 203 else:
194 layout_result = { 204 layout_result = {
195 'content': render_result['segments']['content'], 205 'content': render_result['segments']['content'],
196 'pass_info': None} 206 'pass_info': None}
197 207
198 rp = RenderedPage(page, ctx.uri, ctx.page_num) 208 rp = RenderedPage(qpage)
199 rp.data = page_data 209 rp.data = page_data
200 rp.content = layout_result['content'] 210 rp.content = layout_result['content']
201 rp.render_info[PASS_FORMATTING] = _unpickle_object( 211 rp.render_info[PASS_FORMATTING] = _unpickle_object(
202 render_result['pass_info']) 212 render_result['pass_info'])
203 if layout_result['pass_info'] is not None: 213 if layout_result['pass_info'] is not None:
204 rp.render_info[PASS_RENDERING] = _unpickle_object( 214 rp.render_info[PASS_RENDERING] = _unpickle_object(
205 layout_result['pass_info']) 215 layout_result['pass_info'])
206 return rp 216 return rp
217
207 except Exception as ex: 218 except Exception as ex:
208 if ctx.app.debug: 219 if ctx.app.debug:
209 raise 220 raise
210 logger.exception(ex) 221 logger.exception(ex)
211 page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir) 222 page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir)
212 raise Exception("Error rendering page: %s" % page_rel_path) from ex 223 raise Exception("Error rendering page: %s" % page_rel_path) from ex
224
213 finally: 225 finally:
214 ctx.setCurrentPass(PASS_NONE) 226 ctx.setCurrentPass(PASS_NONE)
215 eis.popPage() 227 stack.popCtx()
216 228
217 229
218 def render_page_segments(ctx): 230 def render_page_segments(ctx):
219 eis = ctx.app.env.exec_info_stack 231 env = ctx.app.env
220 eis.pushPage(ctx.page, ctx) 232
233 stack = env.render_ctx_stack
234 stack.pushCtx(ctx)
235
236 qpage = ctx.qualified_page
237
221 try: 238 try:
222 ctx.setCurrentPass(PASS_FORMATTING) 239 ctx.setCurrentPass(PASS_FORMATTING)
223 repo = ctx.app.env.rendered_segments_repository 240 repo = ctx.app.env.rendered_segments_repository
224 save_to_fs = True 241 save_to_fs = True
225 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: 242 if ctx.app.env.fs_cache_only_for_main_page and not stack.is_main_ctx:
226 save_to_fs = False 243 save_to_fs = False
227 with ctx.app.env.timerScope("PageRenderSegments"): 244 with ctx.app.env.timerScope("PageRenderSegments"):
228 if repo and not ctx.force_render: 245 if repo is not None and not ctx.force_render:
229 render_result = repo.get( 246 render_result = repo.get(
230 ctx.uri, 247 qpage.uri,
231 lambda: _do_render_page_segments_from_ctx(ctx), 248 lambda: _do_render_page_segments_from_ctx(ctx),
232 fs_cache_time=ctx.page.path_mtime, 249 fs_cache_time=qpage.page.content_mtime,
233 save_to_fs=save_to_fs) 250 save_to_fs=save_to_fs)
234 else: 251 else:
235 render_result = _do_render_page_segments_from_ctx(ctx) 252 render_result = _do_render_page_segments_from_ctx(ctx)
236 if repo: 253 if repo:
237 repo.put(ctx.uri, render_result, save_to_fs) 254 repo.put(qpage.uri, render_result, save_to_fs)
238 finally: 255 finally:
239 ctx.setCurrentPass(PASS_NONE) 256 ctx.setCurrentPass(PASS_NONE)
240 eis.popPage() 257 stack.popCtx()
241 258
242 rs = RenderedSegments( 259 rs = RenderedSegments(
243 render_result['segments'], 260 render_result['segments'],
244 _unpickle_object(render_result['pass_info'])) 261 _unpickle_object(render_result['pass_info']))
245 return rs 262 return rs
246 263
247 264
248 def _build_render_data(ctx): 265 def _build_render_data(ctx):
249 with ctx.app.env.timerScope("PageDataBuild"): 266 with ctx.app.env.timerScope("PageDataBuild"):
250 data_ctx = DataBuildingContext(ctx.page, page_num=ctx.page_num) 267 data_ctx = DataBuildingContext(ctx.qualified_page)
251 data_ctx.pagination_source = ctx.pagination_source 268 data_ctx.pagination_source = ctx.pagination_source
252 data_ctx.pagination_filter = ctx.pagination_filter 269 data_ctx.pagination_filter = ctx.pagination_filter
253 page_data = build_page_data(data_ctx) 270 page_data = build_page_data(data_ctx)
254 if ctx.custom_data: 271 if ctx.custom_data:
255 page_data._appendMapping(ctx.custom_data) 272 page_data._appendMapping(ctx.custom_data)
256 return page_data 273 return page_data
257 274
258 275
259 def _do_render_page_segments_from_ctx(ctx): 276 def _do_render_page_segments_from_ctx(ctx):
260 page_data = _build_render_data(ctx) 277 page_data = _build_render_data(ctx)
261 return _do_render_page_segments(ctx.page, page_data) 278 return _do_render_page_segments(ctx, page_data)
262 279
263 280
264 def _do_render_page_segments(page, page_data): 281 def _do_render_page_segments(ctx, page_data):
282 page = ctx.qualified_page.page
265 app = page.app 283 app = page.app
266
267 cpi = app.env.exec_info_stack.current_page_info
268 assert cpi is not None
269 assert cpi.page == page
270 284
271 engine_name = page.config.get('template_engine') 285 engine_name = page.config.get('template_engine')
272 format_name = page.config.get('format') 286 format_name = page.config.get('format')
273 287
274 engine = get_template_engine(app, engine_name) 288 engine = get_template_engine(app, engine_name)
280 part_format = seg_part.fmt or format_name 294 part_format = seg_part.fmt or format_name
281 try: 295 try:
282 with app.env.timerScope( 296 with app.env.timerScope(
283 engine.__class__.__name__ + '_segment'): 297 engine.__class__.__name__ + '_segment'):
284 part_text = engine.renderSegmentPart( 298 part_text = engine.renderSegmentPart(
285 page.path, seg_part, page_data) 299 page.path, seg_part, page_data)
286 except TemplatingError as err: 300 except TemplatingError as err:
287 err.lineno += seg_part.line 301 err.lineno += seg_part.line
288 raise err 302 raise err
289 303
290 part_text = format_text(app, part_format, part_text) 304 part_text = format_text(app, part_format, part_text)
296 if m: 310 if m:
297 offset = m.start() 311 offset = m.start()
298 content_abstract = seg_text[:offset] 312 content_abstract = seg_text[:offset]
299 formatted_segments['content.abstract'] = content_abstract 313 formatted_segments['content.abstract'] = content_abstract
300 314
301 pass_info = cpi.render_ctx.render_passes[PASS_FORMATTING] 315 pass_info = ctx.render_passes[PASS_FORMATTING]
302 res = { 316 res = {
303 'segments': formatted_segments, 317 'segments': formatted_segments,
304 'pass_info': _pickle_object(pass_info)} 318 'pass_info': _pickle_object(pass_info)}
305 return res 319 return res
306 320
307 321
308 def _do_render_layout(layout_name, page, layout_data): 322 def _do_render_layout(layout_name, page, layout_data):
309 cpi = page.app.env.exec_info_stack.current_page_info 323 cpi = page.app.env.exec_info_stack.current_page_info