Mercurial > piecrust2
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('<null>') 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():