diff piecrust/dataproviders/page_iterator.py @ 853:f070a4fc033c

core: Continue PieCrust3 refactor, simplify pages. The asset pipeline is still the only function pipeline at this point. * No more `QualifiedPage`, and several other pieces of code deleted. * Data providers are simpler and more focused. For instance, the page iterator doesn't try to support other types of items. * Route parameters are proper known source metadata to remove the confusion between the two. * Make the baker and pipeline more correctly manage records and record histories. * Add support for record collapsing and deleting stale outputs in the asset pipeline.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 21 May 2017 00:06:59 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/dataproviders/page_iterator.py	Sun May 21 00:06:59 2017 -0700
@@ -0,0 +1,345 @@
+import logging
+from piecrust.data.filters import PaginationFilter
+from piecrust.data.paginationdata import PaginationData
+from piecrust.events import Event
+from piecrust.dataproviders.base import DataProvider
+from piecrust.sources.base import AbortedSourceUseError
+
+
+logger = logging.getLogger(__name__)
+
+
+class PageIteratorDataProvider(DataProvider):
+    """ A data provider that reads a content source as a list of pages.
+
+        This class supports wrapping another `PageIteratorDataProvider`
+        instance because several sources may want to be merged under the
+        same data endpoint (e.g. `site.pages` which lists both the user
+        pages and the theme pages).
+    """
+    PROVIDER_NAME = 'page_iterator'
+
+    debug_render_doc_dynamic = ['_debugRenderDoc']
+    debug_render_not_empty = True
+
+    def __init__(self, source, current_page=None):
+        super().__init__(source)
+        self._it = PageIterator(source, current_page=current_page)
+        self._it._iter_event += self._onIteration
+        self._innerProvider = None
+        self._iterated = False
+
+    def __len__(self):
+        res = len(self._it)
+        if self._innerProvider is not None:
+            res += len(self._innerProvider)
+        return res
+
+    def __iter__(self):
+        yield from self._it
+        if self._innerProvider is not None:
+            yield from self._innerProvider
+
+    def _onIteration(self):
+        if not self._iterated:
+            rcs = self._source.app.env.render_ctx_stack
+            rcs.current_ctx.addUsedSource(self._source.name)
+            self._iterated = True
+
+    def _debugRenderDoc(self):
+        return 'Provides a list of %d items' % len(self)
+
+
+class PageIterator:
+    def __init__(self, source, *,
+                 current_page=None, locked=False):
+        self._source = source
+        self._cache = None
+        self._pagination_slicer = None
+        self._has_sorter = False
+        self._next_page = None
+        self._prev_page = None
+        self._locked = locked
+        self._iter_event = Event()
+        self._current_page = current_page
+        self._it = PageContentSourceIterator(self._source)
+
+    @property
+    def total_count(self):
+        self._load()
+        if self._pagination_slicer is not None:
+            return self._pagination_slicer.inner_count
+        return len(self._cache)
+
+    @property
+    def next_page(self):
+        self._load()
+        return self._next_page
+
+    @property
+    def prev_page(self):
+        self._load()
+        return self._prev_page
+
+    def __len__(self):
+        self._load()
+        return len(self._cache)
+
+    def __getitem__(self, key):
+        self._load()
+        return self._cache[key]
+
+    def __iter__(self):
+        self._load()
+        return iter(self._cache)
+
+    def __getattr__(self, name):
+        if name[:3] == 'is_' or name[:3] == 'in_':
+            def is_filter(value):
+                conf = {'is_%s' % name[3:]: value}
+                return self._simpleNonSortedWrap(SettingFilterIterator, conf)
+            return is_filter
+
+        if name[:4] == 'has_':
+            def has_filter(value):
+                conf = {name: value}
+                return self._simpleNonSortedWrap(SettingFilterIterator, conf)
+            return has_filter
+
+        if name[:5] == 'with_':
+            def has_filter(value):
+                conf = {'has_%s' % name[5:]: value}
+                return self._simpleNonSortedWrap(SettingFilterIterator, conf)
+            return has_filter
+
+        return self.__getattribute__(name)
+
+    def skip(self, count):
+        return self._simpleWrap(SliceIterator, count)
+
+    def limit(self, count):
+        return self._simpleWrap(SliceIterator, 0, count)
+
+    def slice(self, skip, limit):
+        return self._simpleWrap(SliceIterator, skip, limit)
+
+    def filter(self, filter_name):
+        if self._current_page is None:
+            raise Exception("Can't use `filter()` because no parent page was "
+                            "set for this page iterator.")
+        filter_conf = self._current_page.config.get(filter_name)
+        if filter_conf is None:
+            raise Exception("Couldn't find filter '%s' in the configuration "
+                            "header for page: %s" %
+                            (filter_name, self._current_page.path))
+        return self._simpleNonSortedWrap(SettingFilterIterator, filter_conf)
+
+    def sort(self, setting_name, reverse=False):
+        if not setting_name:
+            raise Exception("You need to specify a configuration setting "
+                            "to sort by.")
+        self._ensureUnlocked()
+        self._ensureUnloaded()
+        self._pages = SettingSortIterator(self._pages, setting_name, reverse)
+        self._has_sorter = True
+        return self
+
+    def reset(self):
+        self._ensureUnlocked()
+        self._unload()
+        return self
+
+    @property
+    def _is_loaded(self):
+        return self._cache is not None
+
+    @property
+    def _has_more(self):
+        if self._cache is None:
+            return False
+        if self._pagination_slicer:
+            return self._pagination_slicer.has_more
+        return False
+
+    def _simpleWrap(self, it_class, *args, **kwargs):
+        self._ensureUnlocked()
+        self._ensureUnloaded()
+        self._ensureSorter()
+        self._it = it_class(self._it, *args, **kwargs)
+        if self._pagination_slicer is None and it_class is SliceIterator:
+            self._pagination_slicer = self._it
+            self._pagination_slicer.current_page = self._current_page
+        return self
+
+    def _simpleNonSortedWrap(self, it_class, *args, **kwargs):
+        self._ensureUnlocked()
+        self._ensureUnloaded()
+        self._it = it_class(self._it, *args, **kwargs)
+        return self
+
+    def _ensureUnlocked(self):
+        if self._locked:
+            raise Exception(
+                "This page iterator has been locked and can't be modified.")
+
+    def _ensureUnloaded(self):
+        if self._cache:
+            raise Exception(
+                "This page iterator has already been iterated upon and "
+                "can't be modified anymore.")
+
+    def _ensureSorter(self):
+        if self._has_sorter:
+            return
+        self._it = DateSortIterator(self._it, reverse=True)
+        self._has_sorter = True
+
+    def _unload(self):
+        self._it = PageContentSourceIterator(self._source)
+        self._cache = None
+        self._paginationSlicer = None
+        self._has_sorter = False
+        self._next_page = None
+        self._prev_page = None
+
+    def _load(self):
+        if self._cache is not None:
+            return
+
+        if self._source.app.env.abort_source_use:
+            if self._current_page is not None:
+                logger.debug("Aborting iteration of '%s' from: %s." %
+                             (self.source.name,
+                              self._current_page.content_spec))
+            else:
+                logger.debug("Aborting iteration of '%s'." %
+                             self._source.name)
+            raise AbortedSourceUseError()
+
+        self._ensureSorter()
+
+        tail_it = PaginationDataBuilderIterator(self._it, self._source.route)
+        self._cache = list(tail_it)
+
+        if (self._current_page is not None and
+                self._pagination_slicer is not None):
+            pn = [self._pagination_slicer.prev_page,
+                  self._pagination_slicer.next_page]
+            pn_it = PaginationDataBuilderIterator(iter(pn),
+                                                  self._source.route)
+            self._prev_page, self._next_page = (list(pn_it))
+
+        self._iter_event.fire()
+
+    def _debugRenderDoc(self):
+        return "Contains %d items" % len(self)
+
+
+class SettingFilterIterator:
+    def __init__(self, it, fil_conf):
+        self.it = it
+        self.fil_conf = fil_conf
+        self._fil = None
+
+    def __iter__(self):
+        if self._fil is None:
+            self._fil = PaginationFilter()
+            self._fil.addClausesFromConfig(self.fil_conf)
+
+        for i in self.it:
+            if self._fil.pageMatches(i):
+                yield i
+
+
+class SliceIterator:
+    def __init__(self, it, offset=0, limit=-1):
+        self.it = it
+        self.offset = offset
+        self.limit = limit
+        self.current_page = None
+        self.has_more = False
+        self.inner_count = -1
+        self.next_page = None
+        self.prev_page = None
+        self._cache = None
+
+    def __iter__(self):
+        if self._cache is None:
+            inner_list = list(self.it)
+            self.inner_count = len(inner_list)
+
+            if self.limit > 0:
+                self.has_more = self.inner_count > (self.offset + self.limit)
+                self._cache = inner_list[self.offset:self.offset + self.limit]
+            else:
+                self.has_more = False
+                self._cache = inner_list[self.offset:]
+
+            if self.current_page:
+                try:
+                    idx = inner_list.index(self.current_page)
+                except ValueError:
+                    idx = -1
+                if idx >= 0:
+                    if idx < self.inner_count - 1:
+                        self.next_page = inner_list[idx + 1]
+                    if idx > 0:
+                        self.prev_page = inner_list[idx - 1]
+
+        return iter(self._cache)
+
+
+class SettingSortIterator:
+    def __init__(self, it, name, reverse=False):
+        self.it = it
+        self.name = name
+        self.reverse = reverse
+
+    def __iter__(self):
+        return iter(sorted(self.it, key=self._key_getter,
+                           reverse=self.reverse))
+
+    def _key_getter(self, item):
+        key = item.config.get(item)
+        if key is None:
+            return 0
+        return key
+
+
+class DateSortIterator:
+    def __init__(self, it, reverse=True):
+        self.it = it
+        self.reverse = reverse
+
+    def __iter__(self):
+        return iter(sorted(self.it,
+                           key=lambda x: x.datetime, reverse=self.reverse))
+
+
+class PageContentSourceIterator:
+    def __init__(self, source):
+        self.source = source
+
+        # This is to permit recursive traversal of the
+        # iterator chain. It acts as the end.
+        self.it = None
+
+    def __iter__(self):
+        source = self.source
+        app = source.app
+        for item in source.getAllContents():
+            yield app.getPage(source, item)
+
+
+class PaginationDataBuilderIterator:
+    def __init__(self, it, route):
+        self.it = it
+        self.route = route
+
+    def __iter__(self):
+        for page in self.it:
+            if page is not None:
+                yield PaginationData(page)
+            else:
+                yield None
+