changeset 237:879fe1457e48

data: `Linker` refactor. * Unify the `Linker` and `RecursiveLinker`. * When a page and a directory share the same name, merge their entries in the returned iterator. * Tentative new templating interface.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 15 Feb 2015 22:46:23 -0800
parents eaf18442bff8
children 4dce0e61b48c
files piecrust/data/builder.py piecrust/data/linker.py tests/test_data_linker.py
diffstat 3 files changed, 176 insertions(+), 132 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/data/builder.py	Sun Feb 15 22:42:58 2015 -0800
+++ b/piecrust/data/builder.py	Sun Feb 15 22:46:23 2015 -0800
@@ -5,7 +5,7 @@
 from piecrust.configuration import merge_dicts
 from piecrust.data.assetor import Assetor
 from piecrust.data.debug import build_debug_info
-from piecrust.data.linker import Linker, RecursiveLinker
+from piecrust.data.linker import Linker
 from piecrust.data.paginator import Paginator
 from piecrust.uriutil import get_slug, get_first_sub_uri
 
@@ -36,7 +36,7 @@
     paginator = Paginator(page, pgn_source, first_uri, ctx.page_num,
                           ctx.pagination_filter)
     assetor = Assetor(page, first_uri)
-    recursive_linker = RecursiveLinker(page.source, page_path=page.rel_path)
+    recursive_linker = Linker(page.source, page_path=page.rel_path)
     data = {
             'piecrust': pc_data,
             'page': dict(page.config.get()),
--- a/piecrust/data/linker.py	Sun Feb 15 22:42:58 2015 -0800
+++ b/piecrust/data/linker.py	Sun Feb 15 22:46:23 2015 -0800
@@ -9,21 +9,46 @@
 
 
 class LinkedPageData(PaginationData):
-    debug_render = ['name', 'is_dir', 'is_self']
+    """ 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'] + PaginationData.debug_render
 
-    def __init__(self, name, page, is_self=False):
+    def __init__(self, page):
         super(LinkedPageData, self).__init__(page)
-        self.name = name
-        self.is_self = is_self
+        self.name = page.config.get('__linker_name')
+        self.is_self = page.config.get('__linker_is_self')
+        self.children = page.config.get('__linker_child')
+        self.is_dir = (self.children is not None)
+        self.is_page = True
+
+        self.mapLoader('*', self._linkerChildLoader)
+
+    def _linkerChildLoader(self, name):
+        return getattr(self.children, name)
 
-    @property
-    def is_dir(self):
-        return False
+
+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):
-    def __init__(self, pages):
-        self._pages = pages
+    """ 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()
@@ -32,141 +57,157 @@
         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 None
+        return LinkedPageDataBuilderIterator(it)
 
     def getPaginationFilter(self, page):
         return None
 
     def getSettingAccessor(self):
-        return lambda i, n: i.get(n)
-
-
-class LinkedPageDataIterator(object):
-    def __init__(self, items):
-        self._items = list(items)
-        self._index = -1
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        self._index += 1
-        if self._index >= len(self._items):
-            raise StopIteration()
-        return self._items[self._index]
-
-    def sort(self, name):
-        def key_getter(item):
-            return item[name]
-        self._items = sorted(self._item, key=key_getter)
-        return self
+        if self._orig_source:
+            return self._orig_source.getSettingAccessor()
+        return None
 
 
 class Linker(object):
     debug_render_doc = """Provides access to sibling and children pages."""
 
     def __init__(self, source, *, name=None, dir_path=None, page_path=None):
-        self.source = source
+        self._source = source
         self._name = name
         self._dir_path = dir_path
         self._root_page_path = page_path
-        self._cache = None
-        self._is_listable = None
-
-    def __iter__(self):
-        self._load()
-        return LinkedPageDataIterator(self._cache.values())
-
-    def __getattr__(self, name):
-        self._load()
-        try:
-            return self._cache[name]
-        except KeyError:
-            raise AttributeError()
-
-    @property
-    def name(self):
-        if not self._name:
-            self._load()
-        return self._name
-
-    @property
-    def is_dir(self):
-        return True
-
-    @property
-    def is_self(self):
-        return False
+        self._items = None
 
-    def _load(self):
-        if self._cache is not None:
-            return
-
-        self._is_listable = isinstance(self.source, IListableSource)
-        if self._is_listable and self._root_page_path is not None:
-            if self._name is None:
-                self._name = self.source.getBasename(self._root_page_path)
-            if self._dir_path is None:
-                self._dir_path = self.source.getDirpath(self._root_page_path)
-
-        self._cache = collections.OrderedDict()
-        if not self._is_listable or self._dir_path is None:
-            return
-
-        items = self.source.listPath(self._dir_path)
-        with self.source.app.env.page_repository.startBatchGet():
-            for is_dir, name, data in items:
-                if is_dir:
-                    self._cache[name] = Linker(self.source,
-                                               name=name, dir_path=data)
-                else:
-                    page = data.buildPage()
-                    is_root_page = (self._root_page_path == data.rel_path)
-                    self._cache[name] = LinkedPageData(name, page,
-                                                       is_root_page)
-
-
-class RecursiveLinker(Linker):
-    def __init__(self, source, *args, **kwargs):
-        super(RecursiveLinker, self).__init__(source, *args, **kwargs)
+        self.is_dir = True
+        self.is_page = False
+        self.is_self = False
 
     def __iter__(self):
         return iter(self.pages)
 
     def __getattr__(self, name):
-        if name == 'pages':
-            return self.getpages()
-        if name == 'siblings':
-            return self.getsiblings()
-        raise AttributeError()
+        self._load()
+        try:
+            item = self._items[name]
+        except KeyError:
+            raise AttributeError()
+
+        if isinstance(item, Linker):
+            return item
+
+        return LinkedPageData(item)
+
+    @property
+    def name(self):
+        if self._name is None:
+            self._load()
+        return self._name
+
+    @property
+    def children(self):
+        return self._iterItems(0)
+
+    @property
+    def pages(self):
+        return self._iterItems(0, filter_page_items)
 
-    def getpages(self):
-        src = LinkerSource(self._iterateLinkers())
-        return PageIterator(src)
+    @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)
 
-    def getsiblings(self):
-        src = LinkerSource(self._iterateLinkers(0))
+    @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,
+                      name='.', dir_path=rel_path,
+                      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 frompath(self, rel_path):
-        return RecursiveLinker(self.source, name='.', dir_path=rel_path)
+    def _load(self):
+        if self._items is not None:
+            return
+
+        is_listable = isinstance(self._source, IListableSource)
+        if not is_listable:
+            raise Exception("Source '%s' can't be listed." % self._source.name)
+
+        if self._root_page_path is not None:
+            if self._name is None:
+                self._name = self._source.getBasename(self._root_page_path)
+            if self._dir_path is None:
+                self._dir_path = self._source.getDirpath(self._root_page_path)
+
+        if self._dir_path is None:
+            raise Exception("This linker has no directory to start from.")
 
-    def _iterateLinkers(self, max_depth=-1):
-        self._load()
-        if not self._is_listable:
-            return
-        yield from walk_linkers(self, 0, max_depth)
+        items = list(self._source.listPath(self._dir_path))
+        self._items = collections.OrderedDict()
+        with self._source.app.env.page_repository.startBatchGet():
+            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,
+                                  name=name, dir_path=data)
+                else:
+                    item = data.buildPage()
+                    item.config.set('__linker_name', name)
+                    item.config.set('__linker_is_self',
+                                    item.rel_path == self._root_page_path)
+
+                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.config.set('__linker_child', item)
+                else:
+                    # The current item is a page. The existing item should
+                    # be a directory.
+                    item.config.set('__linker_child', existing)
+                    self._items[name] = item
 
 
-def walk_linkers(linker, depth=0, max_depth=-1):
+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._cache.values():
-        if item.is_dir:
-            if max_depth < 0 or depth + 1 <= max_depth:
-                yield from walk_linkers(item, depth + 1, max_depth)
-        else:
+    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)
+
--- a/tests/test_data_linker.py	Sun Feb 15 22:42:58 2015 -0800
+++ b/tests/test_data_linker.py	Sun Feb 15 22:46:23 2015 -0800
@@ -1,5 +1,5 @@
 import pytest
-from piecrust.data.linker import Linker, RecursiveLinker
+from piecrust.data.linker import Linker
 from .mockutil import mock_fs, mock_fs_scope
 
 
@@ -7,28 +7,32 @@
     'fs, page_path, expected',
     [
         (mock_fs().withPage('pages/foo'), 'foo.md',
-            [('/foo', True, False)]),
+            # is_dir, name, is_self, data
+            [(False, 'foo', True, '/foo')]),
         ((mock_fs()
                 .withPage('pages/foo')
                 .withPage('pages/bar')),
             'foo.md',
-            [('/bar', False, False), ('/foo', True, False)]),
+            [(False, 'bar', False, '/bar'), (False, 'foo', True, '/foo')]),
         ((mock_fs()
                 .withPage('pages/baz')
+                .withPage('pages/something')
                 .withPage('pages/something/else')
                 .withPage('pages/foo')
                 .withPage('pages/bar')),
             'foo.md',
-            [('/bar', False, False), ('/baz', False, False),
-                ('/foo', True, False), ('something', False, True)]),
+            [(False, 'bar', False, '/bar'),
+                (False, 'baz', False, '/baz'),
+                (False, 'foo', True, '/foo'),
+                (True, 'something', False, '/something')]),
         ((mock_fs()
                 .withPage('pages/something/else')
                 .withPage('pages/foo')
                 .withPage('pages/something/good')
                 .withPage('pages/bar')),
             'something/else.md',
-            [('/something/else', True, False),
-                ('/something/good', False, False)])
+            [(False, 'else', True, '/something/else'),
+                (False, 'good', False, '/something/good')])
     ])
 def test_linker_iteration(fs, page_path, expected):
     with mock_fs_scope(fs):
@@ -38,13 +42,12 @@
         actual = list(iter(linker))
 
         assert len(actual) == len(expected)
-        for i, (a, e) in enumerate(zip(actual, expected)):
-            assert a.is_dir == e[2]
-            if a.is_dir:
-                assert a.name == e[0]
-            else:
-                assert a.url == e[0]
-                assert a.is_self == e[1]
+        for (a, e) in zip(actual, expected):
+            is_dir, name, is_self, url = e
+            assert a.is_dir == is_dir
+            assert a.name == name
+            assert a.is_self == is_self
+            assert a.url == url
 
 
 @pytest.mark.parametrize(
@@ -78,8 +81,8 @@
     with mock_fs_scope(fs):
         app = fs.getApp()
         src = app.getSource('pages')
-        linker = RecursiveLinker(src, page_path=page_path)
-        actual = list(iter(linker))
+        linker = Linker(src, page_path=page_path)
+        actual = list(iter(linker.allpages))
 
         assert len(actual) == len(expected)
         for i, (a, e) in enumerate(zip(actual, expected)):