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