view piecrust/templating/jinja/environment.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 3e69f18912f5
children
line wrap: on
line source

import re
import time
import email.utils
import hashlib
import logging
import strict_rfc3339
from jinja2 import Environment
from .extensions import get_highlight_css
from piecrust.data.paginator import Paginator
from piecrust.rendering import format_text
from piecrust.uriutil import multi_replace


logger = logging.getLogger(__name__)


class PieCrustEnvironment(Environment):
    def __init__(self, app, *args, **kwargs):
        self.app = app

        # Before we create the base Environement, let's figure out the options
        # we want to pass to it.
        #
        # Disable auto-reload when we're baking.
        if app.config.get('baker/is_baking'):
            kwargs.setdefault('auto_reload', False)

        # Don't unload templates from the cache.
        kwargs.setdefault('cache_size', -1)

        # Let the user override most Jinja options via the site config.
        for name in ['block_start_string', 'block_end_string',
                     'variable_start_string', 'variable_end_string',
                     'comment_start_string', 'comment_end_string',
                     'line_statement_prefix', 'line_comment_prefix',
                     'trim_blocks', 'lstrip_blocks',
                     'newline_sequence', 'keep_trailing_newline']:
            val = app.config.get('jinja/' + name)
            if val is not None:
                kwargs.setdefault(name, val)

        # Undefined behaviour.
        undef = app.config.get('jinja/undefined')
        if undef == 'logging':
            from jinja2 import make_logging_undefined
            kwargs.setdefault('undefined',
                              make_logging_undefined(logger))
        elif undef == 'strict':
            from jinja2 import StrictUndefined
            kwargs.setdefault('undefined', StrictUndefined)

        # All good! Create the Environment.
        super(PieCrustEnvironment, self).__init__(*args, **kwargs)

        # Now add globals and filters.
        self.globals.update({
            'now': get_now_date(),
            'fail': raise_exception,
            'highlight_css': get_highlight_css})

        self.filters.update({
            'keys': get_dict_keys,
            'values': get_dict_values,
            'paginate': self._paginate,
            'formatwith': self._formatWith,
            'markdown': lambda v: self._formatWith(v, 'markdown'),
            'textile': lambda v: self._formatWith(v, 'textile'),
            'nocache': add_no_cache_parameter,
            'wordcount': get_word_count,
            'stripoutertag': strip_outer_tag,
            'stripslash': strip_slash,
            'titlecase': title_case,
            'md5': make_md5,
            'atomdate': get_xml_date,
            'xmldate': get_xml_date,
            'emaildate': get_email_date,
            'date': get_date})

        self.filters['raw'] = self.filters['safe']

    def _paginate(self, value, items_per_page=5):
        ctx = self.app.env.render_ctx_stack.current_ctx
        if ctx is None or ctx.page is None:
            raise Exception("Can't paginate when no page has been pushed "
                            "on the execution stack.")
        return Paginator(value, ctx.page, ctx.sub_num,
                         items_per_page=items_per_page)

    def _formatWith(self, value, format_name):
        return format_text(self.app, format_name, value)


def raise_exception(msg):
    raise Exception(msg)


def get_dict_keys(value):
    if isinstance(value, list):
        return [i[0] for i in value]
    return value.keys()


def get_dict_values(value):
    if isinstance(value, list):
        return [i[1] for i in value]
    return value.values()


def add_no_cache_parameter(value, param_name='t', param_value=None):
    if not param_value:
        param_value = time.time()
    if '?' in value:
        value += '&'
    else:
        value += '?'
    value += '%s=%s' % (param_name, param_value)
    return value


def get_word_count(value):
    return len(value.split())


def strip_outer_tag(value, tag=None):
    tag_pattern = '[a-z]+[a-z0-9]*'
    if tag is not None:
        tag_pattern = re.escape(tag)
    pat = r'^\<' + tag_pattern + r'\>(.*)\</' + tag_pattern + '>$'
    m = re.match(pat, value)
    if m:
        return m.group(1)
    return value


def strip_slash(value):
    return value.rstrip('/')


def title_case(value):
    return value.title()


def make_md5(value):
    return hashlib.md5(value.lower().encode('utf8')).hexdigest()


def get_xml_date(value):
    """ Formats timestamps like 1985-04-12T23:20:50.52Z
    """
    if value == 'now':
        value = time.time()
    return strict_rfc3339.timestamp_to_rfc3339_localoffset(int(value))


def get_email_date(value, localtime=False):
    """ Formats timestamps like Fri, 09 Nov 2001 01:08:47 -0000
    """
    if value == 'now':
        value = time.time()
    return email.utils.formatdate(value, localtime=localtime)


def get_now_date():
    return time.time()


def get_date(value, fmt):
    if value == 'now':
        value = time.time()
    if '%' not in fmt:
        suggest = php_format_to_strftime_format(fmt)
        if suggest != fmt:
            suggest_message = ("You probably want a format that looks "
                               "like: '%s'." % suggest)
        else:
            suggest_message = ("We can't suggest a proper date format "
                               "for you right now, though.")
        raise Exception("Got incorrect date format: '%s\n"
                        "PieCrust 1 date formats won't work in PieCrust 2. "
                        "%s\n"
                        "Please check the `strftime` formatting page here: "
                        "https://docs.python.org/3/library/datetime.html"
                        "#strftime-and-strptime-behavior" %
                        (fmt, suggest_message))
    return time.strftime(fmt, time.localtime(value))


def php_format_to_strftime_format(fmt):
    replacements = {
        'd': '%d',
        'D': '%a',
        'j': '%d',
        'l': '%A',
        'w': '%w',
        'z': '%j',
        'W': '%W',
        'F': '%B',
        'm': '%m',
        'M': '%b',
        'n': '%m',
        'y': '%Y',
        'Y': '%y',
        'g': '%I',
        'G': '%H',
        'h': '%I',
        'H': '%H',
        'i': '%M',
        's': '%S',
        'e': '%Z',
        'O': '%z',
        'c': '%Y-%m-%dT%H:%M:%SZ'}
    return multi_replace(fmt, replacements)