comparison 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
comparison
equal deleted inserted replaced
853:f070a4fc033c 854:08e02c2a2a1a
1 import logging 1 import logging
2 import collections
3 from piecrust.data.pagedata import LazyPageConfigLoaderHasNoValue
4 from piecrust.data.paginationdata import PaginationData 2 from piecrust.data.paginationdata import PaginationData
5 from piecrust.dataproviders.page_iterator import PageIterator 3 from piecrust.sources.base import (
4 REL_PARENT_GROUP, REL_LOGICAL_PARENT_ITEM, REL_LOGICAl_CHILD_GROUP)
6 5
7 6
8 logger = logging.getLogger(__name__) 7 logger = logging.getLogger(__name__)
9 8
10 9
11 class PageLinkerData(object): 10 _unloaded = object()
12 """ Entry template data to get access to related pages from a given 11
13 root page. 12
13 class Linker:
14 """ A template-exposed data class that lets the user navigate the
15 logical hierarchy of pages in a page source.
14 """ 16 """
15 debug_render = ['parent', 'ancestors', 'siblings', 'children', 'root', 17 debug_render = ['parent', 'ancestors', 'siblings', 'children', 'root',
16 'forpath'] 18 'forpath']
17 debug_render_invoke = ['parent', 'ancestors', 'siblings', 'children', 19 debug_render_invoke = ['parent', 'ancestors', 'siblings', 'children',
18 'root'] 20 'root']
20 'ancestors': '_debugRenderAncestors', 22 'ancestors': '_debugRenderAncestors',
21 'siblings': '_debugRenderSiblings', 23 'siblings': '_debugRenderSiblings',
22 'children': '_debugRenderChildren', 24 'children': '_debugRenderChildren',
23 'root': '_debugRenderRoot'} 25 'root': '_debugRenderRoot'}
24 26
25 def __init__(self, source, page_path): 27 def __init__(self, page):
26 self._source = source 28 self._page = page
27 self._root_page_path = page_path 29 self._content_item = page.content_item
28 self._linker = None 30 self._source = page.source
29 self._is_loaded = False 31 self._app = page.app
32
33 self._parent = _unloaded
34 self._ancestors = None
35 self._siblings = None
36 self._children = None
30 37
31 @property 38 @property
32 def parent(self): 39 def parent(self):
33 self._load() 40 if self._parent is _unloaded:
34 if self._linker is not None: 41 pi = self._source.getRelatedContents(self._content_item,
35 return self._linker.parent 42 REL_LOGICAL_PARENT_ITEM)
36 return None 43 if pi is not None:
44 pipage = self._app.getPage(self._source, pi)
45 self._parent = PaginationData(pipage)
46 else:
47 self._parent = None
48 return self._parent
37 49
38 @property 50 @property
39 def ancestors(self): 51 def ancestors(self):
40 cur = self.parent 52 if self._ancestors is None:
41 while cur: 53 cur_item = self._content_item
42 yield cur 54 self._ancestors = []
43 cur = cur.parent 55 while True:
56 pi = self._source.getRelatedContents(
57 cur_item, REL_LOGICAL_PARENT_ITEM)
58 if pi is not None:
59 pipage = self._app.getPage(self._source, pi)
60 self._ancestors.append(PaginationData(pipage))
61 cur_item = pi
62 else:
63 break
64 return self._ancestors
44 65
45 @property 66 @property
46 def siblings(self): 67 def siblings(self):
47 self._load() 68 if self._siblings is None:
48 if self._linker is None: 69 self._siblings = []
49 return [] 70 parent_group = self._source.getRelatedContents(
50 return self._linker 71 self._content_item, REL_PARENT_GROUP)
72 for i in self._source.getContents(parent_group):
73 if not i.is_group:
74 ipage = self._app.getPage(self._source, i)
75 self._siblings.append(PaginationData(ipage))
76 return self._siblings
51 77
52 @property 78 @property
53 def children(self): 79 def children(self):
54 self._load() 80 if self._children is None:
55 if self._linker is None: 81 self._children = []
56 return [] 82 child_group = self._source.getRelatedContents(
57 self._linker._load() 83 self._content_item, REL_LOGICAl_CHILD_GROUP)
58 if self._linker._self_item is None: 84 if child_group:
59 return [] 85 for i in self._source.getContents(child_group):
60 children = self._linker._self_item._linker_info.child_linker 86 ipage = self._app.getPage(self._source, i)
61 if children is None: 87 self._children.append(PaginationData(ipage))
62 return [] 88 return self._children
63 return children
64
65 @property
66 def root(self):
67 self._load()
68 if self._linker is None:
69 return None
70 return self._linker.root
71
72 def forpath(self, rel_path):
73 self._load()
74 if self._linker is None:
75 return None
76 return self._linker.forpath(rel_path)
77
78 def _load(self):
79 if self._is_loaded:
80 return
81
82 self._is_loaded = True
83
84 dir_path = self._source.getDirpath(self._root_page_path)
85 self._linker = Linker(self._source, dir_path,
86 root_page_path=self._root_page_path)
87 89
88 def _debugRenderAncestors(self): 90 def _debugRenderAncestors(self):
89 return [i.name for i in self.ancestors] 91 return [i.name for i in self.ancestors]
90 92
91 def _debugRenderSiblings(self): 93 def _debugRenderSiblings(self):
98 r = self.root 100 r = self.root
99 if r is not None: 101 if r is not None:
100 return r.name 102 return r.name
101 return None 103 return None
102 104
103
104 class LinkedPageData(PaginationData):
105 """ Class whose instances get returned when iterating on a `Linker`
106 or `RecursiveLinker`. It's just like what gets usually returned by
107 `Paginator` and other page iterators, but with a few additional data
108 like hierarchical data.
109 """
110 debug_render = (['is_dir', 'is_self', 'parent', 'children'] +
111 PaginationData.debug_render)
112 debug_render_invoke = (['is_dir', 'is_self', 'parent', 'children'] +
113 PaginationData.debug_render_invoke)
114
115 def __init__(self, page):
116 super(LinkedPageData, self).__init__(page)
117 self.name = page._linker_info.name
118 self.is_self = page._linker_info.is_self
119 self.is_dir = page._linker_info.is_dir
120 self.is_page = True
121 self._child_linker = page._linker_info.child_linker
122
123 self._mapLoader('*', self._linkerChildLoader)
124
125 @property
126 def parent(self):
127 if self._child_linker is not None:
128 return self._child_linker.parent
129 return None
130
131 @property
132 def children(self):
133 if self._child_linker is not None:
134 return self._child_linker
135 return []
136
137 def _linkerChildLoader(self, data, name):
138 if self.children and hasattr(self.children, name):
139 return getattr(self.children, name)
140 raise LazyPageConfigLoaderHasNoValue
141
142
143 class LinkedPageDataBuilderIterator(object):
144 """ Iterator that builds `LinkedPageData` out of pages.
145 """
146 def __init__(self, it):
147 self.it = it
148
149 def __iter__(self):
150 for item in self.it:
151 yield LinkedPageData(item)
152
153
154 class LinkerSource(IPaginationSource):
155 """ Source iterator that returns pages given by `Linker`.
156 """
157 def __init__(self, pages, orig_source):
158 self._pages = list(pages)
159 self._orig_source = None
160 if isinstance(orig_source, IPaginationSource):
161 self._orig_source = orig_source
162
163 def getItemsPerPage(self):
164 raise NotImplementedError()
165
166 def getSourceIterator(self):
167 return self._pages
168
169 def getSorterIterator(self, it):
170 # We don't want to sort the pages -- we expect the original source
171 # to return hierarchical items in the order it wants already.
172 return None
173
174 def getTailIterator(self, it):
175 return LinkedPageDataBuilderIterator(it)
176
177 def getPaginationFilter(self, page):
178 return None
179
180 def getSettingAccessor(self):
181 if self._orig_source:
182 return self._orig_source.getSettingAccessor()
183 return None
184
185
186 class _LinkerInfo(object):
187 def __init__(self):
188 self.name = None
189 self.is_dir = False
190 self.is_self = False
191 self.child_linker = None
192
193
194 class _LinkedPage(object):
195 def __init__(self, page):
196 self._page = page
197 self._linker_info = _LinkerInfo()
198
199 def __getattr__(self, name):
200 return getattr(self._page, name)
201
202
203 class Linker(object):
204 debug_render_doc = """Provides access to sibling and children pages."""
205
206 def __init__(self, source, dir_path, *, root_page_path=None):
207 self._source = source
208 self._dir_path = dir_path
209 self._root_page_path = root_page_path
210 self._items = None
211 self._parent = None
212 self._self_item = None
213
214 self.is_dir = True
215 self.is_page = False
216 self.is_self = False
217
218 def __iter__(self):
219 return iter(self.pages)
220
221 def __getattr__(self, name):
222 self._load()
223 try:
224 item = self._items[name]
225 except KeyError:
226 raise AttributeError()
227
228 if isinstance(item, Linker):
229 return item
230
231 return LinkedPageData(item)
232
233 def __str__(self):
234 return self.name
235
236 @property
237 def name(self):
238 return self._source.getBasename(self._dir_path)
239
240 @property
241 def children(self):
242 return self._iterItems(0)
243
244 @property
245 def parent(self):
246 if self._dir_path == '':
247 return None
248
249 if self._parent is None:
250 parent_name = self._source.getBasename(self._dir_path)
251 parent_dir_path = self._source.getDirpath(self._dir_path)
252 for is_dir, name, data in self._source.listPath(parent_dir_path):
253 if not is_dir and name == parent_name:
254 parent_page = data.buildPage()
255 item = _LinkedPage(parent_page)
256 item._linker_info.name = parent_name
257 item._linker_info.child_linker = Linker(
258 self._source, parent_dir_path,
259 root_page_path=self._root_page_path)
260 self._parent = LinkedPageData(item)
261 break
262 else:
263 self._parent = Linker(self._source, parent_dir_path,
264 root_page_path=self._root_page_path)
265
266 return self._parent
267
268 @property
269 def pages(self):
270 return self._iterItems(0, filter_page_items)
271
272 @property
273 def directories(self):
274 return self._iterItems(0, filter_directory_items)
275
276 @property
277 def all(self):
278 return self._iterItems()
279
280 @property
281 def allpages(self):
282 return self._iterItems(-1, filter_page_items)
283
284 @property
285 def alldirectories(self):
286 return self._iterItems(-1, filter_directory_items)
287
288 @property
289 def root(self):
290 return self.forpath('/')
291
292 def forpath(self, rel_path):
293 return Linker(self._source, rel_path,
294 root_page_path=self._root_page_path)
295
296 def _iterItems(self, max_depth=-1, filter_func=None):
297 items = walk_linkers(self, max_depth=max_depth,
298 filter_func=filter_func)
299 src = LinkerSource(items, self._source)
300 return PageIterator(src)
301
302 def _load(self):
303 if self._items is not None:
304 return
305
306 items = list(self._source.listPath(self._dir_path))
307 self._items = collections.OrderedDict()
308 for is_dir, name, data in items:
309 # If `is_dir` is true, `data` will be the directory's source
310 # path. If not, it will be a page factory.
311 if is_dir:
312 item = Linker(self._source, data,
313 root_page_path=self._root_page_path)
314 else:
315 page = data.buildPage()
316 is_self = (page.rel_path == self._root_page_path)
317 item = _LinkedPage(page)
318 item._linker_info.name = name
319 item._linker_info.is_self = is_self
320 if is_self:
321 self._self_item = item
322
323 existing = self._items.get(name)
324 if existing is None:
325 self._items[name] = item
326 elif is_dir:
327 # The current item is a directory. The existing item
328 # should be a page.
329 existing._linker_info.child_linker = item
330 existing._linker_info.is_dir = True
331 else:
332 # The current item is a page. The existing item should
333 # be a directory.
334 item._linker_info.child_linker = existing
335 item._linker_info.is_dir = True
336 self._items[name] = item
337
338
339 def filter_page_items(item):
340 return not isinstance(item, Linker)
341
342
343 def filter_directory_items(item):
344 return isinstance(item, Linker)
345
346
347 def walk_linkers(linker, depth=0, max_depth=-1, filter_func=None):
348 linker._load()
349 for item in linker._items.values():
350 if not filter_func or filter_func(item):
351 yield item
352
353 if (isinstance(item, Linker) and
354 (max_depth < 0 or depth + 1 <= max_depth)):
355 yield from walk_linkers(item, depth + 1, max_depth)
356