Mercurial > piecrust2
view piecrust/rendering.py @ 1188:a7c43131d871
bake: Fix file write flushing problem with Python 3.8+
Writing the cache files fails in Python 3.8 because it looks like flushing
behaviour has changed. We need to explicitly flush. And even then, in very
rare occurrences, it looks like it can still run into racing conditions,
so we do a very hacky and ugly "retry" loop when fetching cached data :(
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Tue, 15 Jun 2021 22:36:23 -0700 |
parents | e94737572542 |
children |
line wrap: on
line source
import re import os.path import logging from piecrust.data.builder import ( DataBuildingContext, build_page_data, add_layout_data) from piecrust.templating.base import TemplateNotFoundError, TemplatingError from piecrust.sources.base import AbortedSourceUseError logger = logging.getLogger(__name__) content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$', re.MULTILINE) class RenderingError(Exception): pass class TemplateEngineNotFound(Exception): pass class RenderedSegments(object): def __init__(self, segments, used_templating=False): self.segments = segments self.used_templating = used_templating class RenderedLayout(object): def __init__(self, content): self.content = content class RenderedPage(object): def __init__(self, page, sub_num): self.page = page self.sub_num = sub_num self.data = None self.content = None self.render_info = {} @property def app(self): return self.page.app def create_render_info(): """ Creates a bag of rendering properties. It's a dictionary because it will be passed between workers during the bake process, and saved to records. """ return { 'used_source_names': {'segments': [], 'layout': []}, 'used_pagination': False, 'pagination_has_items': False, 'pagination_has_more': False, 'used_assets': False, } class RenderingContext(object): def __init__(self, page, *, sub_num=1, force_render=False): self.page = page self.sub_num = sub_num self.force_render = force_render self.pagination_source = None self.pagination_filter = None self.render_info = create_render_info() self.custom_data = {} self._current_used_source_names = None @property def app(self): return self.page.app @property def current_used_source_names(self): usn = self._current_used_source_names if usn is not None: return usn else: raise Exception("No render pass specified.") def setRenderPass(self, name): if name is not None: self._current_used_source_names = \ self.render_info['used_source_names'][name] else: self._current_used_source_names = None def setPagination(self, paginator): ri = self.render_info if ri.get('used_pagination'): raise Exception("Pagination has already been used.") assert paginator.is_loaded ri['used_pagination'] = True ri['pagination_has_items'] = paginator.has_items ri['pagination_has_more'] = paginator.has_more self.addUsedSource(paginator._source) def addUsedSource(self, source): usn = self.current_used_source_names if source.name not in usn: usn.append(source.name) class RenderingContextStack(object): def __init__(self): self._ctx_stack = [] @property def is_empty(self): return len(self._ctx_stack) == 0 @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.page == page: return True return False def pushCtx(self, render_ctx): for ctx in self._ctx_stack: if ctx.page == render_ctx.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): env = ctx.app.env stats = env.stats stack = env.render_ctx_stack stack.pushCtx(ctx) page = ctx.page page_uri = page.getUri(ctx.sub_num) try: # Build the data for both segment and layout rendering. with stats.timerScope("BuildRenderData"): page_data = _build_render_data(ctx) # Render content segments. repo = env.rendered_segments_repository save_to_fs = True if env.fs_cache_only_for_main_page and not stack.is_main_ctx: save_to_fs = False with stats.timerScope("PageRenderSegments"): if repo is not None and not ctx.force_render: render_result = repo.get( page_uri, lambda: _do_render_page_segments(ctx, page_data), fs_cache_time=page.content_mtime, save_to_fs=save_to_fs) else: render_result = _do_render_page_segments(ctx, page_data) if repo: repo.put(page_uri, render_result, save_to_fs) # Render layout. layout_name = page.config.get('layout') if layout_name is None: layout_name = page.source.config.get( 'default_layout', 'default') null_names = ['', 'none', 'nil'] if layout_name not in null_names: with stats.timerScope("BuildRenderData"): add_layout_data(page_data, render_result.segments) with stats.timerScope("PageRenderLayout"): layout_result = _do_render_layout( layout_name, page, page_data) else: layout_result = RenderedLayout( render_result.segments['content']) rp = RenderedPage(page, ctx.sub_num) rp.data = page_data rp.content = layout_result.content rp.render_info = ctx.render_info return rp except AbortedSourceUseError: raise except Exception as ex: if ctx.app.debug: raise logger.exception(ex) raise Exception("Error rendering page: %s" % ctx.page.content_spec) from ex finally: stack.popCtx() def render_page_segments(ctx): env = ctx.app.env stats = env.stats stack = env.render_ctx_stack if env.abort_source_use and not stack.is_empty: cur_spec = ctx.page.content_spec from_spec = stack.current_ctx.page.content_spec logger.debug("Aborting rendering of '%s' from: %s." % (cur_spec, from_spec)) raise AbortedSourceUseError() stack.pushCtx(ctx) page = ctx.page page_uri = page.getUri(ctx.sub_num) try: repo = env.rendered_segments_repository save_to_fs = True if env.fs_cache_only_for_main_page and not stack.is_main_ctx: save_to_fs = False with stats.timerScope("PageRenderSegments"): if repo is not None and not ctx.force_render: render_result = repo.get( page_uri, lambda: _do_render_page_segments_from_ctx(ctx), fs_cache_time=page.content_mtime, save_to_fs=save_to_fs) else: render_result = _do_render_page_segments_from_ctx(ctx) if repo: repo.put(page_uri, render_result, save_to_fs) finally: stack.popCtx() return render_result def _build_render_data(ctx): data_ctx = DataBuildingContext(ctx.page, ctx.sub_num) data_ctx.pagination_source = ctx.pagination_source data_ctx.pagination_filter = ctx.pagination_filter page_data = build_page_data(data_ctx) if ctx.custom_data: page_data._appendMapping(ctx.custom_data) return page_data def _do_render_page_segments_from_ctx(ctx): page_data = _build_render_data(ctx) return _do_render_page_segments(ctx, page_data) def _do_render_page_segments(ctx, page_data): page = ctx.page app = page.app ctx.setRenderPass('segments') engine_name = page.config.get('template_engine') format_name = page.config.get('format') engine = get_template_engine(app, engine_name) used_templating = False formatted_segments = {} for seg_name, seg in page.segments.items(): try: with app.env.stats.timerScope( engine.__class__.__name__ + '_segment'): seg_text, was_rendered = engine.renderSegment( page.content_spec, seg, page_data) if was_rendered: used_templating = True except TemplatingError as err: err.lineno += seg.line raise err seg_format = seg.fmt or format_name seg_text = format_text(app, seg_format, seg_text) formatted_segments[seg_name] = seg_text if seg_name == 'content': m = content_abstract_re.search(seg_text) if m: offset = m.start() content_abstract = seg_text[:offset] formatted_segments['content.abstract'] = content_abstract res = RenderedSegments(formatted_segments, used_templating) app.env.stats.stepCounter('PageRenderSegments') return res def _do_render_layout(layout_name, page, layout_data): app = page.app cur_ctx = app.env.render_ctx_stack.current_ctx assert cur_ctx is not None assert cur_ctx.page == page cur_ctx.setRenderPass('layout') names = layout_name.split(',') full_names = [] for name in names: if '.' not in name: full_names.append(name + '.html') else: full_names.append(name) _, engine_name = os.path.splitext(full_names[0]) engine_name = engine_name.lstrip('.') engine = get_template_engine(app, engine_name) try: with app.env.stats.timerScope( engine.__class__.__name__ + '_layout'): output = engine.renderFile(full_names, layout_data) except TemplateNotFoundError as ex: logger.exception(ex) msg = "Can't find template for page: %s\n" % page.content_item.spec msg += "Looked for: %s" % ', '.join(full_names) raise Exception(msg) from ex res = RenderedLayout(output) app.env.stats.stepCounter('PageRenderLayout') return res def get_template_engine(app, engine_name): if engine_name == 'html': engine_name = None engine_name = engine_name or app.config.get('site/default_template_engine') for engine in app.plugin_loader.getTemplateEngines(): if engine_name in engine.ENGINE_NAMES: return engine raise TemplateEngineNotFound("No such template engine: %s" % engine_name) def format_text(app, format_name, txt, exact_format=False): if exact_format and not format_name: raise Exception("You need to specify a format name.") format_count = 0 format_name = format_name or app.config.get('site/default_format') auto_fmts = app.config.get('site/auto_formats') redirect = auto_fmts.get(format_name) if redirect is not None: format_name = redirect for fmt in app.plugin_loader.getFormatters(): if not fmt.enabled: continue if fmt.FORMAT_NAMES is None or format_name in fmt.FORMAT_NAMES: with app.env.stats.timerScope(fmt.__class__.__name__): txt = fmt.render(format_name, txt) format_count += 1 if fmt.OUTPUT_FORMAT is not None: format_name = fmt.OUTPUT_FORMAT if exact_format and format_count == 0: raise Exception("No such format: %s" % format_name) return txt