view piecrust/data/linker.py @ 242:f130365568ff

internal: Code reorganization to put less stuff in `sources.base`. Interfaces that sources can implement are in `sources.interfaces`. The default page source is in `sources.default`. The `SimplePageSource` is gone since most subclasses only wanted to do *some* stuff the same, but *lots* of stuff slightly different. I may have to revisit the code to extract exactly the code that's in common.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 18 Feb 2015 18:35:03 -0800
parents 879fe1457e48
children d9d5c5de02a8
line wrap: on
line source

import logging
import collections
from piecrust.data.base import PaginationData
from piecrust.data.iterators import PageIterator
from piecrust.sources.base import build_pages
from piecrust.sources.interfaces import IPaginationSource, IListableSource


logger = logging.getLogger(__name__)


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'] + PaginationData.debug_render

    def __init__(self, page):
        super(LinkedPageData, self).__init__(page)
        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)


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 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._name = name
        self._dir_path = dir_path
        self._root_page_path = page_path
        self._items = 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)

    @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)

    @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,
                      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 _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.")

        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 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)