Mercurial > piecrust2
diff piecrust/data/base.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 | 5be275137056 |
children | d446029c9478 |
line wrap: on
line diff
--- 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] -