changeset 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 c0700c6d9545
children dc8518c51cbe
files piecrust/baking/worker.py piecrust/configuration.py piecrust/data/base.py piecrust/data/builder.py piecrust/data/debug.py piecrust/data/linker.py piecrust/data/pagedata.py piecrust/data/paginationdata.py piecrust/data/piecrustdata.py piecrust/data/provider.py piecrust/data/providersdata.py piecrust/page.py piecrust/rendering.py piecrust/sources/base.py piecrust/sources/mixins.py piecrust/templating/pystacheengine.py tests/test_configuration.py
diffstat 17 files changed, 467 insertions(+), 388 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/baking/worker.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/baking/worker.py	Sun Jun 28 08:22:39 2015 -0700
@@ -198,7 +198,7 @@
         try:
             page = fac.buildPage()
             page._load()
-            result.config = page.config.get()
+            result.config = page.config.getAll()
         except Exception as ex:
             logger.debug("Got loading error. Sending it to master.")
             result.errors = _get_errors(ex)
--- a/piecrust/configuration.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/configuration.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,7 +1,6 @@
 import re
-import copy
 import logging
-import collections
+import collections.abc
 import yaml
 from yaml.constructor import ConstructorError
 
@@ -15,51 +14,28 @@
     pass
 
 
-class Configuration(object):
+class Configuration(collections.abc.MutableMapping):
     def __init__(self, values=None, validate=True):
         if values is not None:
-            self.setAll(values, validate)
+            self.setAll(values, validate=validate)
         else:
             self._values = None
 
-    def __contains__(self, key):
-        return self.has(key)
-
     def __getitem__(self, key):
-        value = self.get(key)
-        if value is None:
-            raise KeyError()
-        return value
+        self._ensureLoaded()
+        bits = key.split('/')
+        cur = self._values
+        for b in bits:
+            try:
+                cur = cur[b]
+            except KeyError:
+                raise KeyError("No such item: %s" % key)
+        return cur
 
     def __setitem__(self, key, value):
-        return self.set(key, value)
-
-    def setAll(self, values, validate=True):
-        if validate:
-            self._validateAll(values)
-        self._values = values
-
-    def getDeepcopy(self, validate_types=False):
-        if validate_types:
-            self.validateTypes()
-        return copy.deepcopy(self.get())
-
-    def get(self, key_path=None, default_value=None):
         self._ensureLoaded()
-        if key_path is None:
-            return self._values
-        bits = key_path.split('/')
-        cur = self._values
-        for b in bits:
-            cur = cur.get(b)
-            if cur is None:
-                return default_value
-        return cur
-
-    def set(self, key_path, value):
-        self._ensureLoaded()
-        value = self._validateValue(key_path, value)
-        bits = key_path.split('/')
+        value = self._validateValue(key, value)
+        bits = key.split('/')
         bitslen = len(bits)
         cur = self._values
         for i, b in enumerate(bits):
@@ -70,15 +46,31 @@
                     cur[b] = {}
                 cur = cur[b]
 
-    def has(self, key_path):
+    def __delitem__(self, key):
+        raise NotImplementedError()
+
+    def __iter__(self):
+        self._ensureLoaded()
+        return iter(self._values)
+
+    def __len__(self):
         self._ensureLoaded()
-        bits = key_path.split('/')
-        cur = self._values
-        for b in bits:
-            cur = cur.get(b)
-            if cur is None:
-                return False
-        return True
+        return len(self._values)
+
+    def has(self, key):
+        return key in self
+
+    def set(self, key, value):
+        self[key] = value
+
+    def setAll(self, values, validate=False):
+        if validate:
+            self._validateAll(values)
+        self._values = values
+
+    def getAll(self):
+        self._ensureLoaded()
+        return self._values
 
     def merge(self, other):
         self._ensureLoaded()
--- a/piecrust/data/base.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/data/base.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,226 +1,62 @@
-import copy
-import time
-import logging
-from piecrust.data.assetor import Assetor
-from piecrust.routing import create_route_metadata
-from piecrust.uriutil import split_uri
-
-
-logger = logging.getLogger(__name__)
-
-
-class LazyPageConfigLoaderHasNoValue(Exception):
-    """ An exception that can be returned when a loader for `LazyPageConfig`
-        can't return any value.
-    """
-    pass
+import collections.abc
 
 
-class LazyPageConfigData(object):
-    """ 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.
+class MergedMapping(collections.abc.Mapping):
+    """ Provides a dictionary-like object that's really the aggregation of
+        multiple dictionary-like objects.
     """
-    debug_render = []
-    debug_render_invoke = []
-    debug_render_dynamic = ['_debugRenderKeys']
-    debug_render_invoke_dynamic = ['_debugRenderKeys']
-
-    def __init__(self, page):
-        self._page = page
-        self._values = None
-        self._loaders = None
-
-    @property
-    def page(self):
-        return self._page
-
-    def get(self, name):
-        try:
-            return self._getValue(name)
-        except LazyPageConfigLoaderHasNoValue:
-            return None
+    def __init__(self, dicts, path=''):
+        self._dicts = dicts
+        self._path = path
 
     def __getattr__(self, name):
         try:
-            return self._getValue(name)
-        except LazyPageConfigLoaderHasNoValue as ex:
-            raise AttributeError("No such attribute: %s" % name) from ex
+            return self[name]
+        except KeyError:
+            raise AttributeError("No such attribute: %s" % self._subp(name))
 
     def __getitem__(self, name):
-        try:
-            return self._getValue(name)
-        except LazyPageConfigLoaderHasNoValue as ex:
-            raise KeyError("No such key: %s" % name) from ex
-
-    def _getValue(self, name):
-        self._load()
-
-        if name in self._values:
-            return self._values[name]
-
-        if self._loaders:
-            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
-
-                # We need to double-check `_loaders` here because
-                # the loader could have removed all loaders, which
-                # would set this back to `None`.
-                if self._loaders is not None:
-                    del self._loaders[name]
-                    if len(self._loaders) == 0:
-                        self._loaders = None
-
-            else:
-                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.
+        values = []
+        for d in self._dicts:
+            try:
+                val = d[name]
+            except KeyError:
+                continue
+            values.append(val)
 
-        if name not in self._values:
-            raise LazyPageConfigLoaderHasNoValue()
-        return self._values[name]
-
-    def _setValue(self, name, value):
-        if self._values is None:
-            raise Exception("Can't call _setValue before this data has been "
-                            "loaded")
-        self._values[name] = value
-
-    def mapLoader(self, attr_name, loader, override_existing=False):
-        if loader is None:
-            if self._loaders is None or attr_name not in self._loaders:
-                return
-            del self._loaders[attr_name]
-            if len(self._loaders) == 0:
-                self._loaders = None
-            return
+        if len(values) == 0:
+            raise KeyError("No such item: %s" % self._subp(name))
+        if len(values) == 1:
+            return values[0]
 
-        if self._loaders is None:
-            self._loaders = {}
-        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 _load(self):
-        if self._values is not None:
-            return
-        self._values = self._page.config.getDeepcopy(self._page.app.debug)
-        try:
-            self._loadCustom()
-        except Exception as ex:
-            raise Exception(
-                    "Error while loading data for: %s" %
-                    self._page.rel_path) from ex
-
-    def _loadCustom(self):
-        pass
+        for val in values:
+            if not isinstance(val, (dict, collections.abc.Mapping)):
+                raise Exception(
+                        "Template data for '%s' contains an incompatible mix "
+                        "of data: %s" % (
+                            self._subp(name),
+                            ', '.join([str(type(v)) for v in values])))
 
-    def _debugRenderKeys(self):
-        self._load()
-        keys = set(self._values.keys())
-        if self._loaders:
-            keys |= set(self._loaders.keys())
-        return list(keys)
-
-
-class PaginationData(LazyPageConfigData):
-    def __init__(self, page):
-        super(PaginationData, self).__init__(page)
-        self._route = None
-        self._route_metadata = None
+        return MergedMapping(values, self._subp(name))
 
-    def _get_uri(self):
-        page = self._page
-        if self._route is None:
-            # TODO: this is not quite correct, as we're missing parts of the
-            #       route metadata if the current page is a taxonomy page.
-            route_metadata = create_route_metadata(page)
-            self._route = page.app.getRoute(page.source.name, route_metadata)
-            self._route_metadata = route_metadata
-            if self._route is None:
-                raise Exception("Can't get route for page: %s" % page.path)
-        return self._route.getUri(self._route_metadata)
-
-    def _loadCustom(self):
-        page_url = self._get_uri()
-        _, slug = split_uri(self.page.app, page_url)
-        self._setValue('url', page_url)
-        self._setValue('slug', slug)
-        self._setValue(
-                'timestamp',
-                time.mktime(self.page.datetime.timetuple()))
-        date_format = self.page.app.config.get('site/date_format')
-        if date_format:
-            self._setValue('date', self.page.datetime.strftime(date_format))
-        self._setValue('mtime', self.page.path_mtime)
-
-        assetor = Assetor(self.page, page_url)
-        self._setValue('assets', assetor)
+    def __iter__(self):
+        keys = set()
+        for d in self._dicts:
+            keys |= set(d.keys())
+        return iter(keys)
 
-        segment_names = self.page.config.get('segments')
-        for name in segment_names:
-            self.mapLoader(name, self._load_rendered_segment)
-
-    def _load_rendered_segment(self, data, name):
-        do_render = True
-        eis = self._page.app.env.exec_info_stack
-        if eis is not None and eis.hasPage(self._page):
-            # This is the pagination data for the page that is currently
-            # being rendered! Inception! But this is possible... so just
-            # prevent infinite recursion.
-            do_render = False
-
-        assert self is data
+    def __len__(self):
+        keys = set()
+        for d in self._dicts:
+            keys |= set(d.keys())
+        return len(keys)
 
-        if do_render:
-            uri = self._get_uri()
-            try:
-                from piecrust.rendering import (
-                        QualifiedPage, PageRenderingContext,
-                        render_page_segments)
-                qp = QualifiedPage(self._page, self._route,
-                                   self._route_metadata)
-                ctx = PageRenderingContext(qp)
-                render_result = render_page_segments(ctx)
-                segs = render_result.segments
-            except Exception as e:
-                raise Exception(
-                        "Error rendering segments for '%s'" % uri) from e
-        else:
-            segs = {}
-            for name in self.page.config.get('segments'):
-                segs[name] = "<unavailable: current page>"
+    def _subp(self, name):
+        return '%s/%s' % (self._path, name)
 
-        for k, v in segs.items():
-            self.mapLoader(k, None)
-            self._setValue(k, v)
+    def _prependMapping(self, d):
+        self._dicts.insert(0, d)
 
-        if 'content.abstract' in segs:
-            self._setValue('content', segs['content.abstract'])
-            self._setValue('has_more', True)
-            if name == 'content':
-                return segs['content.abstract']
+    def _appendMapping(self, d):
+        self._dicts.append(d)
 
-        return segs[name]
-
--- a/piecrust/data/builder.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/data/builder.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,15 +1,12 @@
-import re
-import time
-import copy
 import logging
 from werkzeug.utils import cached_property
-from piecrust import APP_VERSION
-from piecrust.configuration import merge_dicts
 from piecrust.data.assetor import Assetor
-from piecrust.data.base import LazyPageConfigData
-from piecrust.data.debug import build_debug_info
+from piecrust.data.base import MergedMapping
 from piecrust.data.linker import PageLinkerData
+from piecrust.data.pagedata import PageData
 from piecrust.data.paginator import Paginator
+from piecrust.data.piecrustdata import PieCrustData
+from piecrust.data.providersdata import DataProvidersData
 from piecrust.uriutil import split_sub_uri
 
 
@@ -36,10 +33,10 @@
     app = ctx.app
     page = ctx.page
     first_uri, _ = split_sub_uri(app, ctx.uri)
+    pgn_source = ctx.pagination_source or get_default_pagination_source(page)
 
     pc_data = PieCrustData()
-    config_data = LazyPageConfigData(page)
-    pgn_source = ctx.pagination_source or get_default_pagination_source(page)
+    config_data = PageData(page, ctx)
     paginator = Paginator(page, pgn_source,
                           page_num=ctx.page_num,
                           pgn_filter=ctx.pagination_filter)
@@ -53,20 +50,11 @@
             'family': linker
             }
 
-    for k, v in page.source_metadata.items():
-        config_data.mapValue(k, copy.deepcopy(v))
-    config_data.mapValue('url', ctx.uri, override_existing=True)
-    config_data.mapValue('timestamp', time.mktime(page.datetime.timetuple()),
-                         override_existing=True)
-    date_format = app.config.get('site/date_format')
-    if date_format:
-        config_data.mapValue('date', page.datetime.strftime(date_format),
-                             override_existing=True)
-
     #TODO: handle slugified taxonomy terms.
 
-    site_data = build_site_data(page)
-    merge_dicts(data, site_data)
+    site_data = app.config.getAll()
+    providers_data = DataProvidersData(page)
+    data = MergedMapping([data, providers_data, site_data])
 
     # Do this at the end because we want all the data to be ready to be
     # displayed in the debugger window.
@@ -82,58 +70,7 @@
         if name in page_data:
             logger.warning("Content segment '%s' will hide existing data." %
                            name)
-        page_data[name] = txt
-
-
-class PieCrustData(object):
-    debug_render = ['version', 'url', 'branding', 'debug_info']
-    debug_render_invoke = ['version', 'url', 'branding', 'debug_info']
-    debug_render_redirect = {'debug_info': '_debugRenderDebugInfo'}
-
-    def __init__(self):
-        self.version = APP_VERSION
-        self.url = 'http://bolt80.com/piecrust/'
-        self.branding = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
-                'http://bolt80.com/piecrust/', APP_VERSION)
-        self._page = None
-        self._data = None
-
-    @property
-    def debug_info(self):
-        if self._page is not None and self._data is not None:
-            try:
-                return build_debug_info(self._page, self._data)
-            except Exception as ex:
-                logger.exception(ex)
-                return ('An error occured while generating debug info. '
-                        'Please check the logs.')
-        return ''
-
-    def _enableDebugInfo(self, page, data):
-        self._page = page
-        self._data = data
-
-    def _debugRenderDebugInfo(self):
-        return "The very thing you're looking at!"
-
-
-re_endpoint_sep = re.compile(r'[\/\.]')
-
-
-def build_site_data(page):
-    app = page.app
-    data = app.config.getDeepcopy(app.debug)
-    for source in app.sources:
-        endpoint_bits = re_endpoint_sep.split(source.data_endpoint)
-        endpoint = data
-        for e in endpoint_bits[:-1]:
-            if e not in endpoint:
-                endpoint[e] = {}
-            endpoint = endpoint[e]
-        user_data = endpoint.get(endpoint_bits[-1])
-        provider = source.buildDataProvider(page, user_data)
-        endpoint[endpoint_bits[-1]] = provider
-    return data
+    page_data._prependMapping(contents)
 
 
 def get_default_pagination_source(page):
--- a/piecrust/data/debug.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/data/debug.py	Sun Jun 28 08:22:39 2015 -0700
@@ -3,6 +3,7 @@
 import html
 import logging
 import collections
+import collections.abc
 from piecrust import APP_VERSION, PIECRUST_URL
 from piecrust.page import FLAG_RAW_CACHE_VALID
 
@@ -162,7 +163,7 @@
             self._write('&lt;null&gt;')
             return
 
-        if isinstance(data, dict):
+        if isinstance(data, (dict, collections.abc.Mapping)):
             self._renderCollapsableValueStart(path)
             with IndentScope(self):
                 self._renderDict(data, path)
--- a/piecrust/data/linker.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/data/linker.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,7 +1,8 @@
 import logging
 import collections
-from piecrust.data.base import PaginationData, LazyPageConfigLoaderHasNoValue
 from piecrust.data.iterators import PageIterator
+from piecrust.data.pagedata import LazyPageConfigLoaderHasNoValue
+from piecrust.data.paginationdata import PaginationData
 from piecrust.sources.interfaces import IPaginationSource, IListableSource
 
 
@@ -103,7 +104,7 @@
         self.is_page = True
         self._child_linker = page._linker_info.child_linker
 
-        self.mapLoader('*', self._linkerChildLoader)
+        self._mapLoader('*', self._linkerChildLoader)
 
     @property
     def parent(self):
--- /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))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/data/paginationdata.py	Sun Jun 28 08:22:39 2015 -0700
@@ -0,0 +1,88 @@
+import time
+from piecrust.data.assetor import Assetor
+from piecrust.data.pagedata import LazyPageConfigData
+from piecrust.routing import create_route_metadata
+from piecrust.uriutil import split_uri
+
+
+class PaginationData(LazyPageConfigData):
+    def __init__(self, page):
+        super(PaginationData, self).__init__(page)
+        self._route = None
+        self._route_metadata = None
+
+    def _get_uri(self):
+        page = self._page
+        if self._route is None:
+            # TODO: this is not quite correct, as we're missing parts of the
+            #       route metadata if the current page is a taxonomy page.
+            route_metadata = create_route_metadata(page)
+            self._route = page.app.getRoute(page.source.name, route_metadata)
+            self._route_metadata = route_metadata
+            if self._route is None:
+                raise Exception("Can't get route for page: %s" % page.path)
+        return self._route.getUri(self._route_metadata)
+
+    def _load(self):
+        page = self._page
+        page_url = self._get_uri()
+        _, slug = split_uri(page.app, page_url)
+        self._setValue('url', page_url)
+        self._setValue('slug', slug)
+        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))
+        self._setValue('mtime', page.path_mtime)
+
+        assetor = Assetor(page, page_url)
+        self._setValue('assets', assetor)
+
+        segment_names = page.config.get('segments')
+        for name in segment_names:
+            self._mapLoader(name, self._load_rendered_segment)
+
+    def _load_rendered_segment(self, data, name):
+        do_render = True
+        eis = self._page.app.env.exec_info_stack
+        if eis is not None and eis.hasPage(self._page):
+            # This is the pagination data for the page that is currently
+            # being rendered! Inception! But this is possible... so just
+            # prevent infinite recursion.
+            do_render = False
+
+        assert self is data
+
+        if do_render:
+            uri = self._get_uri()
+            try:
+                from piecrust.rendering import (
+                        QualifiedPage, PageRenderingContext,
+                        render_page_segments)
+                qp = QualifiedPage(self._page, self._route,
+                                   self._route_metadata)
+                ctx = PageRenderingContext(qp)
+                render_result = render_page_segments(ctx)
+                segs = render_result.segments
+            except Exception as e:
+                raise Exception(
+                        "Error rendering segments for '%s'" % uri) from e
+        else:
+            segs = {}
+            for name in self._page.config.get('segments'):
+                segs[name] = "<unavailable: current page>"
+
+        for k, v in segs.items():
+            self._unmapLoader(k)
+            self._setValue(k, v)
+
+        if 'content.abstract' in segs:
+            self._setValue('content', segs['content.abstract'])
+            self._setValue('has_more', True)
+            if name == 'content':
+                return segs['content.abstract']
+
+        return segs[name]
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/data/piecrustdata.py	Sun Jun 28 08:22:39 2015 -0700
@@ -0,0 +1,38 @@
+import logging
+from piecrust import APP_VERSION
+from piecrust.data.debug import build_debug_info
+
+
+logger = logging.getLogger(__name__)
+
+
+class PieCrustData(object):
+    debug_render = ['version', 'url', 'branding', 'debug_info']
+    debug_render_invoke = ['version', 'url', 'branding', 'debug_info']
+    debug_render_redirect = {'debug_info': '_debugRenderDebugInfo'}
+
+    def __init__(self):
+        self.version = APP_VERSION
+        self.url = 'http://bolt80.com/piecrust/'
+        self.branding = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
+                'http://bolt80.com/piecrust/', APP_VERSION)
+        self._page = None
+        self._data = None
+
+    @property
+    def debug_info(self):
+        if self._page is not None and self._data is not None:
+            try:
+                return build_debug_info(self._page, self._data)
+            except Exception as ex:
+                logger.exception(ex)
+                return ('An error occured while generating debug info. '
+                        'Please check the logs.')
+        return ''
+
+    def _enableDebugInfo(self, page, data):
+        self._page = page
+        self._data = data
+
+    def _debugRenderDebugInfo(self):
+        return "The very thing you're looking at!"
--- a/piecrust/data/provider.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/data/provider.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,37 +1,19 @@
 import time
+import collections.abc
 from piecrust.data.iterators import PageIterator
 from piecrust.sources.array import ArraySource
 
 
 class DataProvider(object):
-    debug_render_dynamic = ['_debugRenderUserData']
-    debug_render_invoke_dynamic = ['_debugRenderUserData']
+    debug_render_dynamic = []
+    debug_render_invoke_dynamic = []
 
-    def __init__(self, source, page, user_data):
+    def __init__(self, source, page, override):
         if source.app is not page.app:
             raise Exception("The given source and page don't belong to "
                             "the same application.")
         self._source = source
         self._page = page
-        self._user_data = user_data
-
-    def __getattr__(self, name):
-        if self._user_data is not None:
-            try:
-                return self._user_data[name]
-            except KeyError:
-                pass
-        raise AttributeError()
-
-    def __getitem__(self, name):
-        if self._user_data is not None:
-            return self._user_data[name]
-        raise KeyError()
-
-    def _debugRenderUserData(self):
-        if self._user_data:
-            return list(self._user_data.keys())
-        return []
 
 
 class IteratorDataProvider(DataProvider):
@@ -40,16 +22,16 @@
     debug_render_doc_dynamic = ['_debugRenderDoc']
     debug_render_not_empty = True
 
-    def __init__(self, source, page, user_data):
+    def __init__(self, source, page, override):
+        super(IteratorDataProvider, self).__init__(source, page, override)
+
         self._innerIt = None
-        if isinstance(user_data, IteratorDataProvider):
+        if isinstance(override, IteratorDataProvider):
             # Iterator providers can be chained, like for instance with
             # `site.pages` listing both the theme pages and the user site's
             # pages.
-            self._innerIt = user_data
-            user_data = None
+            self._innerIt = override
 
-        super(IteratorDataProvider, self).__init__(source, page, user_data)
         self._pages = PageIterator(source, current_page=page)
         self._pages._iter_event += self._onIteration
         self._ctx_set = False
@@ -75,44 +57,48 @@
         return 'Provides a list of %d items' % len(self)
 
 
-class BlogDataProvider(DataProvider):
+class BlogDataProvider(DataProvider, collections.abc.Mapping):
     PROVIDER_NAME = 'blog'
 
     debug_render_doc = """Provides a list of blog posts and yearly/monthly
                           archives."""
-    debug_render = ['posts', 'years', 'months']
     debug_render_dynamic = (['_debugRenderTaxonomies'] +
             DataProvider.debug_render_dynamic)
 
-    def __init__(self, source, page, user_data):
-        super(BlogDataProvider, self).__init__(source, page, user_data)
+    def __init__(self, source, page, override):
+        super(BlogDataProvider, self).__init__(source, page, override)
         self._yearly = None
         self._monthly = None
         self._taxonomies = {}
         self._ctx_set = False
 
-    def __getattr__(self, name):
-        if self._source.app.getTaxonomy(name) is not None:
+    def __getitem__(self, name):
+        if name == 'posts':
+            return self._posts()
+        elif name == 'years':
+            return self._buildYearlyArchive()
+        elif name == 'months':
+            return self._buildMonthlyArchive()
+        elif self._source.app.getTaxonomy(name) is not None:
             return self._buildTaxonomy(name)
-        return super(BlogDataProvider, self).__getattr__(name)
+        raise KeyError("No such item: %s" % name)
+
+    def __iter__(self):
+        keys = ['posts', 'years', 'months']
+        keys += [t.name for t in self._source.app.taxonomies]
+        return iter(keys)
 
-    @property
-    def posts(self):
+    def __len__(self):
+        return 3 + len(self._source.app.taxonomies)
+
+    def _debugRenderTaxonomies(self):
+        return [t.name for t in self._source.app.taxonomies]
+
+    def _posts(self):
         it = PageIterator(self._source, current_page=self._page)
         it._iter_event += self._onIteration
         return it
 
-    @property
-    def years(self):
-        return self._buildYearlyArchive()
-
-    @property
-    def months(self):
-        return self._buildMonthlyArchive()
-
-    def _debugRenderTaxonomies(self):
-        return [t.name for t in self._source.app.taxonomies]
-
     def _buildYearlyArchive(self):
         if self._yearly is not None:
             return self._yearly
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/data/providersdata.py	Sun Jun 28 08:22:39 2015 -0700
@@ -0,0 +1,39 @@
+import re
+import collections.abc
+
+
+re_endpoint_sep = re.compile(r'[\/\.]')
+
+
+class DataProvidersData(collections.abc.Mapping):
+    def __init__(self, page):
+        self._page = page
+        self._dict = None
+
+    def __getitem__(self, name):
+        self._load()
+        return self._dict[name]
+
+    def __iter__(self):
+        self._load()
+        return iter(self._dict)
+
+    def __len__(self):
+        self._load()
+        return len(self._dict)
+
+    def _load(self):
+        if self._dict is not None:
+            return
+
+        self._dict = {}
+        for source in self._page.app.sources:
+            endpoint_bits = re_endpoint_sep.split(source.data_endpoint)
+            endpoint = self._dict
+            for e in endpoint_bits[:-1]:
+                if e not in endpoint:
+                    endpoint[e] = {}
+                endpoint = endpoint[e]
+            override = endpoint.get(endpoint_bits[-1])
+            provider = source.buildDataProvider(self._page, override)
+            endpoint[endpoint_bits[-1]] = provider
--- a/piecrust/page.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/page.py	Sun Jun 28 08:22:39 2015 -0700
@@ -284,7 +284,7 @@
 
     # Save to the cache.
     cache_data = {
-            'config': config.get(),
+            'config': config.getAll(),
             'content': json_save_segments(content)}
     cache.write(cache_path, json.dumps(cache_data))
 
--- a/piecrust/rendering.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/rendering.py	Sun Jun 28 08:22:39 2015 -0700
@@ -282,7 +282,7 @@
         data_ctx.pagination_filter = ctx.pagination_filter
         page_data = build_page_data(data_ctx)
         if ctx.custom_data:
-            page_data.update(ctx.custom_data)
+            page_data._appendMapping(ctx.custom_data)
         return page_data
 
 
--- a/piecrust/sources/base.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/sources/base.py	Sun Jun 28 08:22:39 2015 -0700
@@ -114,7 +114,7 @@
     def findPageFactory(self, metadata, mode):
         raise NotImplementedError()
 
-    def buildDataProvider(self, page, user_data):
+    def buildDataProvider(self, page, override):
         if self._provider_type is None:
             cls = next((pt for pt in self.app.plugin_loader.getDataProviders()
                         if pt.PROVIDER_NAME == self.data_type),
@@ -124,7 +124,7 @@
                         "Unknown data provider type: %s" % self.data_type)
             self._provider_type = cls
 
-        return self._provider_type(self, page, user_data)
+        return self._provider_type(self, page, override)
 
     def getTaxonomyPageRef(self, tax_name):
         tax_pages = self.config.get('taxonomy_pages')
--- a/piecrust/sources/mixins.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/sources/mixins.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,8 +1,8 @@
 import os
 import os.path
 import logging
-from piecrust.data.base import PaginationData
 from piecrust.data.filters import PaginationFilter, page_value_accessor
+from piecrust.data.paginationdata import PaginationData
 from piecrust.sources.base import PageFactory
 from piecrust.sources.interfaces import IPaginationSource, IListableSource
 from piecrust.sources.pageref import PageRef
--- a/piecrust/templating/pystacheengine.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/piecrust/templating/pystacheengine.py	Sun Jun 28 08:22:39 2015 -0700
@@ -1,4 +1,5 @@
 import logging
+import collections.abc
 import pystache
 import pystache.common
 from piecrust.templating.base import (
@@ -70,7 +71,9 @@
             # a list. This is just plain wrong, but it will take a while before
             # the project can get patches on Pypi.
             res = mrc(stack, name)
-            if res is not None and res.__class__.__name__ in _knowns:
+            if res is not None and (
+                    res.__class__.__name__ in _knowns or
+                    isinstance(res, collections.abc.Mapping)):
                 res = [res]
             return res
 
--- a/tests/test_configuration.py	Sat Jun 27 22:28:32 2015 -0700
+++ b/tests/test_configuration.py	Sun Jun 28 08:22:39 2015 -0700
@@ -13,13 +13,13 @@
     ])
 def test_config_init(values, expected):
     config = Configuration(values)
-    assert config.get() == expected
+    assert config.getAll() == expected
 
 
 def test_config_set_all():
     config = Configuration()
     config.setAll({'foo': 'bar'})
-    assert config.get() == {'foo': 'bar'}
+    assert config.getAll() == {'foo': 'bar'}
 
 
 def test_config_get_and_set():
@@ -125,7 +125,7 @@
                 'child10': 'ten'
                 }
             }
-    assert config.get() == expected
+    assert config.getAll() == expected
 
 
 def test_ordered_loader():