comparison piecrust/rendering.py @ 854:08e02c2a2a1a

core: Keep refactoring, this time to prepare for generator sources. - Make a few APIs simpler. - Content pipelines create their own jobs, so that generator sources can keep aborting in `getContents`, but rely on their pipeline to generate pages for baking.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 04 Jun 2017 23:34:28 -0700
parents f070a4fc033c
children 58e28ba02fb7
comparison
equal deleted inserted replaced
853:f070a4fc033c 854:08e02c2a2a1a
3 import copy 3 import copy
4 import logging 4 import logging
5 from piecrust.data.builder import ( 5 from piecrust.data.builder import (
6 DataBuildingContext, build_page_data, add_layout_data) 6 DataBuildingContext, build_page_data, add_layout_data)
7 from piecrust.fastpickle import _pickle_object, _unpickle_object 7 from piecrust.fastpickle import _pickle_object, _unpickle_object
8 from piecrust.sources.base import ContentSource
9 from piecrust.templating.base import TemplateNotFoundError, TemplatingError 8 from piecrust.templating.base import TemplateNotFoundError, TemplatingError
10 9
11 10
12 logger = logging.getLogger(__name__) 11 logger = logging.getLogger(__name__)
13 12
113 pass_info.pagination_has_more = paginator.has_more 112 pass_info.pagination_has_more = paginator.has_more
114 self.addUsedSource(paginator._source) 113 self.addUsedSource(paginator._source)
115 114
116 def addUsedSource(self, source): 115 def addUsedSource(self, source):
117 self._raiseIfNoCurrentPass() 116 self._raiseIfNoCurrentPass()
118 if isinstance(source, ContentSource): 117 pass_info = self.current_pass_info
119 pass_info = self.current_pass_info 118 pass_info.used_source_names.add(source.name)
120 pass_info.used_source_names.add(source.name)
121 119
122 def _raiseIfNoCurrentPass(self): 120 def _raiseIfNoCurrentPass(self):
123 if self._current_pass == PASS_NONE: 121 if self._current_pass == PASS_NONE:
124 raise Exception("No rendering pass is currently active.") 122 raise Exception("No rendering pass is currently active.")
125 123
157 self._ctx_stack = [] 155 self._ctx_stack = []
158 156
159 157
160 def render_page(ctx): 158 def render_page(ctx):
161 env = ctx.app.env 159 env = ctx.app.env
160 stats = env.stats
162 161
163 stack = env.render_ctx_stack 162 stack = env.render_ctx_stack
164 stack.pushCtx(ctx) 163 stack.pushCtx(ctx)
165 164
166 page = ctx.page 165 page = ctx.page
167 page_uri = page.getUri(ctx.sub_num) 166 page_uri = page.getUri(ctx.sub_num)
168 167
169 try: 168 try:
170 # Build the data for both segment and layout rendering. 169 # Build the data for both segment and layout rendering.
171 with env.timerScope("BuildRenderData"): 170 with stats.timerScope("BuildRenderData"):
172 page_data = _build_render_data(ctx) 171 page_data = _build_render_data(ctx)
173 172
174 # Render content segments. 173 # Render content segments.
175 ctx.setCurrentPass(PASS_FORMATTING) 174 ctx.setCurrentPass(PASS_FORMATTING)
176 repo = env.rendered_segments_repository 175 repo = env.rendered_segments_repository
177 save_to_fs = True 176 save_to_fs = True
178 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: 177 if env.fs_cache_only_for_main_page and not stack.is_main_ctx:
179 save_to_fs = False 178 save_to_fs = False
180 with env.timerScope("PageRenderSegments"): 179 with stats.timerScope("PageRenderSegments"):
181 if repo is not None and not ctx.force_render: 180 if repo is not None and not ctx.force_render:
182 render_result = repo.get( 181 render_result = repo.get(
183 page_uri, 182 page_uri,
184 lambda: _do_render_page_segments(ctx, page_data), 183 lambda: _do_render_page_segments(ctx, page_data),
185 fs_cache_time=page.content_mtime, 184 fs_cache_time=page.content_mtime,
195 if layout_name is None: 194 if layout_name is None:
196 layout_name = page.source.config.get( 195 layout_name = page.source.config.get(
197 'default_layout', 'default') 196 'default_layout', 'default')
198 null_names = ['', 'none', 'nil'] 197 null_names = ['', 'none', 'nil']
199 if layout_name not in null_names: 198 if layout_name not in null_names:
200 with ctx.app.env.timerScope("BuildRenderData"): 199 with stats.timerScope("BuildRenderData"):
201 add_layout_data(page_data, render_result['segments']) 200 add_layout_data(page_data, render_result['segments'])
202 201
203 with ctx.app.env.timerScope("PageRenderLayout"): 202 with stats.timerScope("PageRenderLayout"):
204 layout_result = _do_render_layout( 203 layout_result = _do_render_layout(
205 layout_name, page, page_data) 204 layout_name, page, page_data)
206 else: 205 else:
207 layout_result = { 206 layout_result = {
208 'content': render_result['segments']['content'], 207 'content': render_result['segments']['content'],
220 219
221 except Exception as ex: 220 except Exception as ex:
222 if ctx.app.debug: 221 if ctx.app.debug:
223 raise 222 raise
224 logger.exception(ex) 223 logger.exception(ex)
225 page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir) 224 raise Exception("Error rendering page: %s" %
226 raise Exception("Error rendering page: %s" % page_rel_path) from ex 225 ctx.page.content_spec) from ex
227 226
228 finally: 227 finally:
229 ctx.setCurrentPass(PASS_NONE) 228 ctx.setCurrentPass(PASS_NONE)
230 stack.popCtx() 229 stack.popCtx()
231 230
232 231
233 def render_page_segments(ctx): 232 def render_page_segments(ctx):
234 env = ctx.app.env 233 env = ctx.app.env
234 stats = env.stats
235 235
236 stack = env.render_ctx_stack 236 stack = env.render_ctx_stack
237 stack.pushCtx(ctx) 237 stack.pushCtx(ctx)
238 238
239 page = ctx.page 239 page = ctx.page
240 page_uri = page.getUri(ctx.sub_num) 240 page_uri = page.getUri(ctx.sub_num)
241 241
242 try: 242 try:
243 ctx.setCurrentPass(PASS_FORMATTING) 243 ctx.setCurrentPass(PASS_FORMATTING)
244 repo = ctx.app.env.rendered_segments_repository 244 repo = env.rendered_segments_repository
245
245 save_to_fs = True 246 save_to_fs = True
246 if ctx.app.env.fs_cache_only_for_main_page and not stack.is_main_ctx: 247 if env.fs_cache_only_for_main_page and not stack.is_main_ctx:
247 save_to_fs = False 248 save_to_fs = False
248 with ctx.app.env.timerScope("PageRenderSegments"): 249
250 with stats.timerScope("PageRenderSegments"):
249 if repo is not None and not ctx.force_render: 251 if repo is not None and not ctx.force_render:
250 render_result = repo.get( 252 render_result = repo.get(
251 page_uri, 253 page_uri,
252 lambda: _do_render_page_segments_from_ctx(ctx), 254 lambda: _do_render_page_segments_from_ctx(ctx),
253 fs_cache_time=page.content_mtime, 255 fs_cache_time=page.content_mtime,
265 _unpickle_object(render_result['pass_info'])) 267 _unpickle_object(render_result['pass_info']))
266 return rs 268 return rs
267 269
268 270
269 def _build_render_data(ctx): 271 def _build_render_data(ctx):
270 with ctx.app.env.timerScope("PageDataBuild"): 272 with ctx.app.env.stats.timerScope("PageDataBuild"):
271 data_ctx = DataBuildingContext(ctx.page, ctx.sub_num) 273 data_ctx = DataBuildingContext(ctx.page, ctx.sub_num)
272 data_ctx.pagination_source = ctx.pagination_source 274 data_ctx.pagination_source = ctx.pagination_source
273 data_ctx.pagination_filter = ctx.pagination_filter 275 data_ctx.pagination_filter = ctx.pagination_filter
274 page_data = build_page_data(data_ctx) 276 page_data = build_page_data(data_ctx)
275 if ctx.custom_data: 277 if ctx.custom_data:
295 for seg_name, seg in page.segments.items(): 297 for seg_name, seg in page.segments.items():
296 seg_text = '' 298 seg_text = ''
297 for seg_part in seg.parts: 299 for seg_part in seg.parts:
298 part_format = seg_part.fmt or format_name 300 part_format = seg_part.fmt or format_name
299 try: 301 try:
300 with app.env.timerScope( 302 with app.env.stats.timerScope(
301 engine.__class__.__name__ + '_segment'): 303 engine.__class__.__name__ + '_segment'):
302 part_text = engine.renderSegmentPart( 304 part_text = engine.renderSegmentPart(
303 page.path, seg_part, page_data) 305 page.content_spec, seg_part, page_data)
304 except TemplatingError as err: 306 except TemplatingError as err:
305 err.lineno += seg_part.line 307 err.lineno += seg_part.line
306 raise err 308 raise err
307 309
308 part_text = format_text(app, part_format, part_text) 310 part_text = format_text(app, part_format, part_text)
322 'pass_info': _pickle_object(pass_info)} 324 'pass_info': _pickle_object(pass_info)}
323 return res 325 return res
324 326
325 327
326 def _do_render_layout(layout_name, page, layout_data): 328 def _do_render_layout(layout_name, page, layout_data):
327 cpi = page.app.env.exec_info_stack.current_page_info 329 cur_ctx = page.app.env.render_ctx_stack.current_ctx
328 assert cpi is not None 330 assert cur_ctx is not None
329 assert cpi.page == page 331 assert cur_ctx.page == page
330 332
331 names = layout_name.split(',') 333 names = layout_name.split(',')
332 default_exts = page.app.env.default_layout_extensions
333 full_names = [] 334 full_names = []
334 for name in names: 335 for name in names:
335 if '.' not in name: 336 if '.' not in name:
336 for ext in default_exts: 337 full_names.append(name + '.html')
337 full_names.append(name + ext)
338 else: 338 else:
339 full_names.append(name) 339 full_names.append(name)
340 340
341 _, engine_name = os.path.splitext(full_names[0]) 341 _, engine_name = os.path.splitext(full_names[0])
342 engine_name = engine_name.lstrip('.') 342 engine_name = engine_name.lstrip('.')
343 engine = get_template_engine(page.app, engine_name) 343 engine = get_template_engine(page.app, engine_name)
344 344
345 try: 345 try:
346 with page.app.env.timerScope(engine.__class__.__name__ + '_layout'): 346 with page.app.env.stats.timerScope(
347 engine.__class__.__name__ + '_layout'):
347 output = engine.renderFile(full_names, layout_data) 348 output = engine.renderFile(full_names, layout_data)
348 except TemplateNotFoundError as ex: 349 except TemplateNotFoundError as ex:
349 logger.exception(ex) 350 logger.exception(ex)
350 msg = "Can't find template for page: %s\n" % page.path 351 msg = "Can't find template for page: %s\n" % page.path
351 msg += "Looked for: %s" % ', '.join(full_names) 352 msg += "Looked for: %s" % ', '.join(full_names)
352 raise Exception(msg) from ex 353 raise Exception(msg) from ex
353 354
354 pass_info = cpi.render_ctx.render_passes[PASS_RENDERING] 355 pass_info = cur_ctx.render_passes[PASS_RENDERING]
355 res = {'content': output, 'pass_info': _pickle_object(pass_info)} 356 res = {'content': output, 'pass_info': _pickle_object(pass_info)}
356 return res 357 return res
357 358
358 359
359 def get_template_engine(app, engine_name): 360 def get_template_engine(app, engine_name):
374 format_name = format_name or app.config.get('site/default_format') 375 format_name = format_name or app.config.get('site/default_format')
375 for fmt in app.plugin_loader.getFormatters(): 376 for fmt in app.plugin_loader.getFormatters():
376 if not fmt.enabled: 377 if not fmt.enabled:
377 continue 378 continue
378 if fmt.FORMAT_NAMES is None or format_name in fmt.FORMAT_NAMES: 379 if fmt.FORMAT_NAMES is None or format_name in fmt.FORMAT_NAMES:
379 with app.env.timerScope(fmt.__class__.__name__): 380 with app.env.stats.timerScope(fmt.__class__.__name__):
380 txt = fmt.render(format_name, txt) 381 txt = fmt.render(format_name, txt)
381 format_count += 1 382 format_count += 1
382 if fmt.OUTPUT_FORMAT is not None: 383 if fmt.OUTPUT_FORMAT is not None:
383 format_name = fmt.OUTPUT_FORMAT 384 format_name = fmt.OUTPUT_FORMAT
384 if exact_format and format_count == 0: 385 if exact_format and format_count == 0: