view piecrust/data/pagedata.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 45ad976712ec
children
line wrap: on
line source

import copy
import time
import logging
import collections.abc
from piecrust.sources.base import AbortedSourceUseError


logger = logging.getLogger(__name__)


class LazyPageConfigLoaderHasNoValue(Exception):
    """ An exception that can be returned when a loader for `LazyPageConfig`
        can't return any value.
    """
    pass


class LazyPageConfigData(collections.abc.Mapping):
    """ An object that represents the configuration header of a page,
        but also allows for additional data. It's meant to be exposed
        to the templating system.
    """
    debug_render = []
    debug_render_invoke = []
    debug_render_dynamic = ['_debugRenderKeys']
    debug_render_invoke_dynamic = ['_debugRenderKeys']

    def __init__(self, page):
        self._page = page
        self._values = {}
        self._loaders = {}
        self._is_loaded = False

    def __getattr__(self, name):
        try:
            return self._getValue(name)
        except LazyPageConfigLoaderHasNoValue as ex:
            raise AttributeError("No such attribute: %s" % name) from ex

    def __getitem__(self, name):
        try:
            return self._getValue(name)
        except LazyPageConfigLoaderHasNoValue as ex:
            raise KeyError("No such key: %s" % name) from ex

    def __iter__(self):
        keys = set(self._page.config.keys())
        keys |= set(self._values.keys())
        keys |= set(self._loaders.keys())
        keys.discard('*')
        return iter(keys)

    def __len__(self):
        return len(self._page.config) + len(self._values) + len(self._loaders)

    def _getValue(self, name):
        # First try the page configuration itself.
        try:
            return self._page.config[name]
        except KeyError:
            pass

        # Then try loaded values.
        self._ensureLoaded()
        try:
            return self._values[name]
        except KeyError:
            pass

        # Try a loader for a new value.
        loader = self._loaders.get(name)
        if loader is not None:
            try:
                with self._page.app.env.stats.timerScope('BuildLazyPageData'):
                    self._values[name] = loader(self, name)
            except (LazyPageConfigLoaderHasNoValue, AbortedSourceUseError):
                raise
            except Exception as ex:
                logger.exception(ex)
                raise Exception(
                    "Error while loading attribute '%s' for: %s" %
                    (name, self._page.content_spec)) from ex

            # Forget this loader now that it served its purpose.
            try:
                del self._loaders[name]
            except KeyError:
                pass
            return self._values[name]

        # Try the wildcard loader if it exists.
        loader = self._loaders.get('*')
        if loader is not None:
            try:
                with self._page.app.env.stats.timerScope('BuildLazyPageData'):
                    self._values[name] = loader(self, name)
            except (LazyPageConfigLoaderHasNoValue, AbortedSourceUseError):
                raise
            except Exception as ex:
                logger.exception(ex)
                raise Exception(
                    "Error while loading attribute '%s' for: %s" %
                    (name, self._page.content_spec)) from ex
            # We always keep the wildcard loader in the loaders list.
            try:
                return self._values[name]
            except KeyError:
                pass

        raise LazyPageConfigLoaderHasNoValue()

    def _setValue(self, name, value):
        self._values[name] = value

    def _unmapLoader(self, attr_name):
        try:
            del self._loaders[attr_name]
        except KeyError:
            pass

    def _mapLoader(self, attr_name, loader, override_existing=False):
        assert loader is not None

        if not override_existing and attr_name in self._loaders:
            raise Exception(
                "A loader has already been mapped for: %s" % attr_name)
        self._loaders[attr_name] = loader

    def _mapValue(self, attr_name, value, override_existing=False):
        self._mapLoader(
            attr_name,
            lambda _, __: value,
            override_existing=override_existing)

    def _ensureLoaded(self):
        if self._is_loaded:
            return

        self._is_loaded = True
        try:
            with self._page.app.env.stats.timerScope('BuildLazyPageData'):
                self._load()
        except Exception as ex:
            logger.exception(ex)
            raise Exception(
                "Error while loading data for: %s" %
                self._page.content_spec) from ex

    def _load(self):
        pass

    def _debugRenderKeys(self):
        self._ensureLoaded()
        keys = set(self._values.keys())
        if self._loaders:
            keys |= set(self._loaders.keys())
            keys.discard('*')
        return list(keys)


class PageData(LazyPageConfigData):
    """ Template data for a page.
    """
    def __init__(self, page, ctx):
        super().__init__(page)
        self._ctx = ctx

    def _load(self):
        from piecrust.uriutil import split_uri

        page = self._page
        set_val = self._setValue

        page_url = page.getUri(self._ctx.sub_num)
        _, rel_url = split_uri(page.app, page_url)

        dt = page.datetime
        for k, v in page.source_metadata.items():
            set_val(k, v)
        set_val('url', page_url)
        set_val('rel_url', rel_url)
        set_val('route', copy.deepcopy(page.source_metadata['route_params']))

        set_val('timestamp', time.mktime(dt.timetuple()))
        set_val('datetime', {
            'year': dt.year, 'month': dt.month, 'day': dt.day,
            'hour': dt.hour, 'minute': dt.minute, 'second': dt.second})

        self._mapLoader('date', _load_date)


def _load_date(data, name):
    page = data._page
    date_format = page.app.config.get('site/date_format')
    if date_format:
        return page.datetime.strftime(date_format)
    return None