diff piecrust/data/pagedata.py @ 440:32c7c2d219d2

performance: Refactor how data is managed to reduce copying. * Make use of `collections.abc.Mapping` to better identify things that are supposed to look like dictionaries. * Instead of handling "overlay" of data in a dict tree in each different data object, make all objects `Mapping`s and handle merging at a higher level with the new `MergedMapping` object. * Since this new object is read-only, remove the need for deep-copying of app and page configurations. * Split data classes into separate modules.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 28 Jun 2015 08:22:39 -0700
parents
children 785dea918ad8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/data/pagedata.py	Sun Jun 28 08:22:39 2015 -0700
@@ -0,0 +1,158 @@
+import time
+import collections.abc
+
+
+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 = list(self._page.config.keys())
+        keys += list(self._values.keys())
+        keys += list(self._loaders.keys())
+        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:
+                self._values[name] = loader(self, name)
+            except LazyPageConfigLoaderHasNoValue:
+                raise
+            except Exception as ex:
+                raise Exception(
+                        "Error while loading attribute '%s' for: %s" %
+                        (name, self._page.rel_path)) 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:
+                self._values[name] = loader(self, name)
+            except LazyPageConfigLoaderHasNoValue:
+                raise
+            except Exception as ex:
+                raise Exception(
+                        "Error while loading attribute '%s' for: %s" %
+                        (name, self._page.rel_path)) from ex
+            # We always keep the wildcard loader in the loaders list.
+            return self._values[name]
+
+        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):
+        loader = lambda _, __: value
+        self._mapLoader(attr_name, loader, override_existing=override_existing)
+
+    def _ensureLoaded(self):
+        if self._is_loaded:
+            return
+
+        self._is_loaded = True
+        try:
+            self._load()
+        except Exception as ex:
+            raise Exception(
+                    "Error while loading data for: %s" %
+                    self._page.rel_path) from ex
+
+    def _load(self):
+        pass
+
+    def _debugRenderKeys(self):
+        self._ensureLoaded()
+        keys = set(self._values.keys())
+        if self._loaders:
+            keys |= set(self._loaders.keys())
+        return list(keys)
+
+
+class PageData(LazyPageConfigData):
+    """ Template data for a page.
+    """
+    def __init__(self, page, ctx):
+        super(PageData, self).__init__(page)
+        self._ctx = ctx
+
+    def _load(self):
+        page = self._page
+        for k, v in page.source_metadata.items():
+            self._setValue(k, v)
+        self._setValue('url', self._ctx.uri)
+        self._setValue('timestamp', time.mktime(page.datetime.timetuple()))
+        date_format = page.app.config.get('site/date_format')
+        if date_format:
+            self._setValue('date', page.datetime.strftime(date_format))
+