diff piecrust/data/linker.py @ 854:08e02c2a2a1a

core: Keep refactoring, this time to prepare for generator sources. - Make a few APIs simpler. - Content pipelines create their own jobs, so that generator sources can keep aborting in `getContents`, but rely on their pipeline to generate pages for baking.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 04 Jun 2017 23:34:28 -0700
parents f070a4fc033c
children fddaf43424e2
line wrap: on
line diff
--- a/piecrust/data/linker.py	Sun May 21 00:06:59 2017 -0700
+++ b/piecrust/data/linker.py	Sun Jun 04 23:34:28 2017 -0700
@@ -1,16 +1,18 @@
 import logging
-import collections
-from piecrust.data.pagedata import LazyPageConfigLoaderHasNoValue
 from piecrust.data.paginationdata import PaginationData
-from piecrust.dataproviders.page_iterator import PageIterator
+from piecrust.sources.base import (
+    REL_PARENT_GROUP, REL_LOGICAL_PARENT_ITEM, REL_LOGICAl_CHILD_GROUP)
 
 
 logger = logging.getLogger(__name__)
 
 
-class PageLinkerData(object):
-    """ Entry template data to get access to related pages from a given
-        root page.
+_unloaded = object()
+
+
+class Linker:
+    """ A template-exposed data class that lets the user navigate the
+        logical hierarchy of pages in a page source.
     """
     debug_render = ['parent', 'ancestors', 'siblings', 'children', 'root',
                     'forpath']
@@ -22,68 +24,68 @@
         'children': '_debugRenderChildren',
         'root': '_debugRenderRoot'}
 
-    def __init__(self, source, page_path):
-        self._source = source
-        self._root_page_path = page_path
-        self._linker = None
-        self._is_loaded = False
+    def __init__(self, page):
+        self._page = page
+        self._content_item = page.content_item
+        self._source = page.source
+        self._app = page.app
+
+        self._parent = _unloaded
+        self._ancestors = None
+        self._siblings = None
+        self._children = None
 
     @property
     def parent(self):
-        self._load()
-        if self._linker is not None:
-            return self._linker.parent
-        return None
+        if self._parent is _unloaded:
+            pi = self._source.getRelatedContents(self._content_item,
+                                                 REL_LOGICAL_PARENT_ITEM)
+            if pi is not None:
+                pipage = self._app.getPage(self._source, pi)
+                self._parent = PaginationData(pipage)
+            else:
+                self._parent = None
+        return self._parent
 
     @property
     def ancestors(self):
-        cur = self.parent
-        while cur:
-            yield cur
-            cur = cur.parent
+        if self._ancestors is None:
+            cur_item = self._content_item
+            self._ancestors = []
+            while True:
+                pi = self._source.getRelatedContents(
+                    cur_item, REL_LOGICAL_PARENT_ITEM)
+                if pi is not None:
+                    pipage = self._app.getPage(self._source, pi)
+                    self._ancestors.append(PaginationData(pipage))
+                    cur_item = pi
+                else:
+                    break
+        return self._ancestors
 
     @property
     def siblings(self):
-        self._load()
-        if self._linker is None:
-            return []
-        return self._linker
+        if self._siblings is None:
+            self._siblings = []
+            parent_group = self._source.getRelatedContents(
+                self._content_item, REL_PARENT_GROUP)
+            for i in self._source.getContents(parent_group):
+                if not i.is_group:
+                    ipage = self._app.getPage(self._source, i)
+                    self._siblings.append(PaginationData(ipage))
+        return self._siblings
 
     @property
     def children(self):
-        self._load()
-        if self._linker is None:
-            return []
-        self._linker._load()
-        if self._linker._self_item is None:
-            return []
-        children = self._linker._self_item._linker_info.child_linker
-        if children is None:
-            return []
-        return children
-
-    @property
-    def root(self):
-        self._load()
-        if self._linker is None:
-            return None
-        return self._linker.root
-
-    def forpath(self, rel_path):
-        self._load()
-        if self._linker is None:
-            return None
-        return self._linker.forpath(rel_path)
-
-    def _load(self):
-        if self._is_loaded:
-            return
-
-        self._is_loaded = True
-
-        dir_path = self._source.getDirpath(self._root_page_path)
-        self._linker = Linker(self._source, dir_path,
-                              root_page_path=self._root_page_path)
+        if self._children is None:
+            self._children = []
+            child_group = self._source.getRelatedContents(
+                self._content_item, REL_LOGICAl_CHILD_GROUP)
+            if child_group:
+                for i in self._source.getContents(child_group):
+                    ipage = self._app.getPage(self._source, i)
+                    self._children.append(PaginationData(ipage))
+        return self._children
 
     def _debugRenderAncestors(self):
         return [i.name for i in self.ancestors]
@@ -100,257 +102,3 @@
             return r.name
         return None
 
-
-class LinkedPageData(PaginationData):
-    """ Class whose instances get returned when iterating on a `Linker`
-        or `RecursiveLinker`. It's just like what gets usually returned by
-        `Paginator` and other page iterators, but with a few additional data
-        like hierarchical data.
-    """
-    debug_render = (['is_dir', 'is_self', 'parent', 'children'] +
-                    PaginationData.debug_render)
-    debug_render_invoke = (['is_dir', 'is_self', 'parent', 'children'] +
-                           PaginationData.debug_render_invoke)
-
-    def __init__(self, page):
-        super(LinkedPageData, self).__init__(page)
-        self.name = page._linker_info.name
-        self.is_self = page._linker_info.is_self
-        self.is_dir = page._linker_info.is_dir
-        self.is_page = True
-        self._child_linker = page._linker_info.child_linker
-
-        self._mapLoader('*', self._linkerChildLoader)
-
-    @property
-    def parent(self):
-        if self._child_linker is not None:
-            return self._child_linker.parent
-        return None
-
-    @property
-    def children(self):
-        if self._child_linker is not None:
-            return self._child_linker
-        return []
-
-    def _linkerChildLoader(self, data, name):
-        if self.children and hasattr(self.children, name):
-            return getattr(self.children, name)
-        raise LazyPageConfigLoaderHasNoValue
-
-
-class LinkedPageDataBuilderIterator(object):
-    """ Iterator that builds `LinkedPageData` out of pages.
-    """
-    def __init__(self, it):
-        self.it = it
-
-    def __iter__(self):
-        for item in self.it:
-            yield LinkedPageData(item)
-
-
-class LinkerSource(IPaginationSource):
-    """ Source iterator that returns pages given by `Linker`.
-    """
-    def __init__(self, pages, orig_source):
-        self._pages = list(pages)
-        self._orig_source = None
-        if isinstance(orig_source, IPaginationSource):
-            self._orig_source = orig_source
-
-    def getItemsPerPage(self):
-        raise NotImplementedError()
-
-    def getSourceIterator(self):
-        return self._pages
-
-    def getSorterIterator(self, it):
-        # We don't want to sort the pages -- we expect the original source
-        # to return hierarchical items in the order it wants already.
-        return None
-
-    def getTailIterator(self, it):
-        return LinkedPageDataBuilderIterator(it)
-
-    def getPaginationFilter(self, page):
-        return None
-
-    def getSettingAccessor(self):
-        if self._orig_source:
-            return self._orig_source.getSettingAccessor()
-        return None
-
-
-class _LinkerInfo(object):
-    def __init__(self):
-        self.name = None
-        self.is_dir = False
-        self.is_self = False
-        self.child_linker = None
-
-
-class _LinkedPage(object):
-    def __init__(self, page):
-        self._page = page
-        self._linker_info = _LinkerInfo()
-
-    def __getattr__(self, name):
-        return getattr(self._page, name)
-
-
-class Linker(object):
-    debug_render_doc = """Provides access to sibling and children pages."""
-
-    def __init__(self, source, dir_path, *, root_page_path=None):
-        self._source = source
-        self._dir_path = dir_path
-        self._root_page_path = root_page_path
-        self._items = None
-        self._parent = None
-        self._self_item = None
-
-        self.is_dir = True
-        self.is_page = False
-        self.is_self = False
-
-    def __iter__(self):
-        return iter(self.pages)
-
-    def __getattr__(self, name):
-        self._load()
-        try:
-            item = self._items[name]
-        except KeyError:
-            raise AttributeError()
-
-        if isinstance(item, Linker):
-            return item
-
-        return LinkedPageData(item)
-
-    def __str__(self):
-        return self.name
-
-    @property
-    def name(self):
-        return self._source.getBasename(self._dir_path)
-
-    @property
-    def children(self):
-        return self._iterItems(0)
-
-    @property
-    def parent(self):
-        if self._dir_path == '':
-            return None
-
-        if self._parent is None:
-            parent_name = self._source.getBasename(self._dir_path)
-            parent_dir_path = self._source.getDirpath(self._dir_path)
-            for is_dir, name, data in self._source.listPath(parent_dir_path):
-                if not is_dir and name == parent_name:
-                    parent_page = data.buildPage()
-                    item = _LinkedPage(parent_page)
-                    item._linker_info.name = parent_name
-                    item._linker_info.child_linker = Linker(
-                        self._source, parent_dir_path,
-                        root_page_path=self._root_page_path)
-                    self._parent = LinkedPageData(item)
-                    break
-            else:
-                self._parent = Linker(self._source, parent_dir_path,
-                                      root_page_path=self._root_page_path)
-
-        return self._parent
-
-    @property
-    def pages(self):
-        return self._iterItems(0, filter_page_items)
-
-    @property
-    def directories(self):
-        return self._iterItems(0, filter_directory_items)
-
-    @property
-    def all(self):
-        return self._iterItems()
-
-    @property
-    def allpages(self):
-        return self._iterItems(-1, filter_page_items)
-
-    @property
-    def alldirectories(self):
-        return self._iterItems(-1, filter_directory_items)
-
-    @property
-    def root(self):
-        return self.forpath('/')
-
-    def forpath(self, rel_path):
-        return Linker(self._source, rel_path,
-                      root_page_path=self._root_page_path)
-
-    def _iterItems(self, max_depth=-1, filter_func=None):
-        items = walk_linkers(self, max_depth=max_depth,
-                             filter_func=filter_func)
-        src = LinkerSource(items, self._source)
-        return PageIterator(src)
-
-    def _load(self):
-        if self._items is not None:
-            return
-
-        items = list(self._source.listPath(self._dir_path))
-        self._items = collections.OrderedDict()
-        for is_dir, name, data in items:
-            # If `is_dir` is true, `data` will be the directory's source
-            # path. If not, it will be a page factory.
-            if is_dir:
-                item = Linker(self._source, data,
-                              root_page_path=self._root_page_path)
-            else:
-                page = data.buildPage()
-                is_self = (page.rel_path == self._root_page_path)
-                item = _LinkedPage(page)
-                item._linker_info.name = name
-                item._linker_info.is_self = is_self
-                if is_self:
-                    self._self_item = item
-
-            existing = self._items.get(name)
-            if existing is None:
-                self._items[name] = item
-            elif is_dir:
-                # The current item is a directory. The existing item
-                # should be a page.
-                existing._linker_info.child_linker = item
-                existing._linker_info.is_dir = True
-            else:
-                # The current item is a page. The existing item should
-                # be a directory.
-                item._linker_info.child_linker = existing
-                item._linker_info.is_dir = True
-                self._items[name] = item
-
-
-def filter_page_items(item):
-    return not isinstance(item, Linker)
-
-
-def filter_directory_items(item):
-    return isinstance(item, Linker)
-
-
-def walk_linkers(linker, depth=0, max_depth=-1, filter_func=None):
-    linker._load()
-    for item in linker._items.values():
-        if not filter_func or filter_func(item):
-            yield item
-
-        if (isinstance(item, Linker) and
-                (max_depth < 0 or depth + 1 <= max_depth)):
-            yield from walk_linkers(item, depth + 1, max_depth)
-