view piecrust/templating/jinjaengine.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 8adc27285d93
children
line wrap: on
line source

import os.path
import logging
from piecrust.sources.base import AbortedSourceUseError
from piecrust.templating.base import (TemplateEngine, TemplateNotFoundError,
                                      TemplatingError)


logger = logging.getLogger(__name__)


class JinjaTemplateEngine(TemplateEngine):
    ENGINE_NAMES = ['jinja', 'jinja2', 'j2']
    EXTENSIONS = ['html', 'jinja', 'jinja2', 'j2']

    def __init__(self):
        self.env = None
        self._jinja_syntax_error = None
        self._jinja_not_found = None

    def renderSegment(self, path, segment, data):
        if not _string_needs_render(segment.content):
            return segment.content, False

        self._ensureLoaded()

        seg_path = _make_segment_path(path, segment.offset)
        self.env.loader.segments_cache[seg_path] = (
            path, segment.content)
        try:
            tpl = self.env.get_template(seg_path)
        except self._jinja_syntax_error as tse:
            raise self._getTemplatingError(tse, filename=path)
        except self._jinja_not_found:
            raise TemplateNotFoundError()

        try:
            return tpl.render(data), True
        except self._jinja_syntax_error as tse:
            raise self._getTemplatingError(tse)
        except AbortedSourceUseError:
            raise
        except Exception as ex:
            if self.app.debug:
                raise
            msg = "Error rendering Jinja markup"
            rel_path = os.path.relpath(path, self.app.root_dir)
            raise TemplatingError(msg, rel_path) from ex

    def renderFile(self, paths, data):
        self._ensureLoaded()

        try:
            tpl = self.env.select_template(paths)
        except self._jinja_syntax_error as tse:
            raise self._getTemplatingError(tse)
        except self._jinja_not_found:
            raise TemplateNotFoundError()

        try:
            return tpl.render(data)
        except self._jinja_syntax_error as tse:
            raise self._getTemplatingError(tse)
        except AbortedSourceUseError:
            raise
        except Exception as ex:
            if self.app.debug:
                raise
            msg = "Error rendering Jinja markup"
            name = getattr(tpl, 'name', '<unknown template>')
            raise TemplatingError(msg, name) from ex

    def _getTemplatingError(self, tse, filename=None):
        filename = tse.filename or filename
        if filename and os.path.isabs(filename):
            filename = os.path.relpath(filename, self.env.app.root_dir)
        err = TemplatingError(str(tse), filename, tse.lineno)
        raise err from tse

    def _ensureLoaded(self):
        if self.env is not None:
            return

        stats = self.app.env.stats
        stats.registerTimer('JinjaTemplateEngine_setup',
                            raise_if_registered=False)
        stats.registerTimer('JinjaTemplateEngine_extensions',
                            raise_if_registered=False)
        with stats.timerScope('JinjaTemplateEngine_setup'):
            self._load()

    def _load(self):
        get_config = self.app.config.get

        # Get the list of extensions to load.
        ext_names = get_config('jinja/extensions', [])
        if not isinstance(ext_names, list):
            ext_names = [ext_names]

        # Turn on autoescape by default.
        autoescape = get_config('jinja/auto_escape', True)
        if autoescape:
            ext_names.append('autoescape')

        # Create the final list of extensions.
        from piecrust.templating.jinja.extensions import (
            PieCrustHighlightExtension, PieCrustCacheExtension,
            PieCrustSpacelessExtension, PieCrustFormatExtension)
        extensions = [
            PieCrustHighlightExtension,
            PieCrustCacheExtension,
            PieCrustSpacelessExtension,
            PieCrustFormatExtension]
        for n in ext_names:
            if '.' not in n:
                n = 'jinja2.ext.' + n
            extensions.append(n)
        for je in self.app.plugin_loader.getTemplateEngineExtensions('jinja'):
            extensions.append(je)

        # Create the Jinja environment.
        logger.debug("Creating Jinja environment with folders: %s" %
                     self.app.templates_dirs)
        from piecrust.templating.jinja.loader import PieCrustLoader
        loader = PieCrustLoader(self.app.templates_dirs)
        from piecrust.templating.jinja.environment import PieCrustEnvironment
        self.env = PieCrustEnvironment(
            self.app,
            loader=loader,
            extensions=extensions)

        # Get types we need later.
        from jinja2 import TemplateNotFound
        from jinja2.exceptions import TemplateSyntaxError
        self._jinja_syntax_error = TemplateSyntaxError
        self._jinja_not_found = TemplateNotFound


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_path(path, start):
    return '$seg=%s:%d' % (path, start)