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