comparison piecrust/dataproviders/pageiterator.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
children 9bb22bbe093c
comparison
equal deleted inserted replaced
853:f070a4fc033c 854:08e02c2a2a1a
1 import logging
2 from piecrust.data.filters import PaginationFilter
3 from piecrust.data.paginationdata import PaginationData
4 from piecrust.events import Event
5 from piecrust.dataproviders.base import DataProvider
6 from piecrust.sources.base import AbortedSourceUseError
7
8
9 logger = logging.getLogger(__name__)
10
11
12 class _ItInfo:
13 def __init__(self):
14 self.it = None
15 self.iterated = False
16
17
18 class PageIteratorDataProvider(DataProvider):
19 """ A data provider that reads a content source as a list of pages.
20
21 This class supports wrapping another `PageIteratorDataProvider`
22 instance because several sources may want to be merged under the
23 same data endpoint (e.g. `site.pages` which lists both the user
24 pages and the theme pages).
25 """
26 PROVIDER_NAME = 'page_iterator'
27
28 debug_render_doc_dynamic = ['_debugRenderDoc']
29 debug_render_not_empty = True
30
31 def __init__(self, source, page):
32 super().__init__(source, page)
33 self._its = None
34 self._app = source.app
35
36 def __len__(self):
37 self._load()
38 return sum([len(i.it) for i in self._its])
39
40 def __iter__(self):
41 self._load()
42 for i in self._its:
43 yield from i.it
44
45 def _load(self):
46 if self._its is not None:
47 return
48
49 self._its = []
50 for source in self._sources:
51 i = _ItInfo()
52 i.it = PageIterator(source, current_page=self._page)
53 i.it._iter_event += self._onIteration
54 self._its.append(i)
55
56 def _onIteration(self, it):
57 ii = next(filter(lambda i: i.it == it, self._its))
58 if not ii.iterated:
59 rcs = self._app.env.render_ctx_stack
60 rcs.current_ctx.addUsedSource(self._source.name)
61 ii.iterated = True
62
63 def _debugRenderDoc(self):
64 return 'Provides a list of %d items' % len(self)
65
66
67 class PageIterator:
68 def __init__(self, source, *, current_page=None):
69 self._source = source
70 self._cache = None
71 self._pagination_slicer = None
72 self._has_sorter = False
73 self._next_page = None
74 self._prev_page = None
75 self._locked = False
76 self._iter_event = Event()
77 self._current_page = current_page
78 self._it = PageContentSourceIterator(self._source)
79
80 @property
81 def total_count(self):
82 self._load()
83 if self._pagination_slicer is not None:
84 return self._pagination_slicer.inner_count
85 return len(self._cache)
86
87 @property
88 def next_page(self):
89 self._load()
90 return self._next_page
91
92 @property
93 def prev_page(self):
94 self._load()
95 return self._prev_page
96
97 def __len__(self):
98 self._load()
99 return len(self._cache)
100
101 def __getitem__(self, key):
102 self._load()
103 return self._cache[key]
104
105 def __iter__(self):
106 self._load()
107 return iter(self._cache)
108
109 def __getattr__(self, name):
110 if name[:3] == 'is_' or name[:3] == 'in_':
111 def is_filter(value):
112 conf = {'is_%s' % name[3:]: value}
113 return self._simpleNonSortedWrap(SettingFilterIterator, conf)
114 return is_filter
115
116 if name[:4] == 'has_':
117 def has_filter(value):
118 conf = {name: value}
119 return self._simpleNonSortedWrap(SettingFilterIterator, conf)
120 return has_filter
121
122 if name[:5] == 'with_':
123 def has_filter(value):
124 conf = {'has_%s' % name[5:]: value}
125 return self._simpleNonSortedWrap(SettingFilterIterator, conf)
126 return has_filter
127
128 return self.__getattribute__(name)
129
130 def skip(self, count):
131 return self._simpleWrap(SliceIterator, count)
132
133 def limit(self, count):
134 return self._simpleWrap(SliceIterator, 0, count)
135
136 def slice(self, skip, limit):
137 return self._simpleWrap(SliceIterator, skip, limit)
138
139 def filter(self, filter_name):
140 if self._current_page is None:
141 raise Exception("Can't use `filter()` because no parent page was "
142 "set for this page iterator.")
143 filter_conf = self._current_page.config.get(filter_name)
144 if filter_conf is None:
145 raise Exception("Couldn't find filter '%s' in the configuration "
146 "header for page: %s" %
147 (filter_name, self._current_page.path))
148 return self._simpleNonSortedWrap(SettingFilterIterator, filter_conf)
149
150 def sort(self, setting_name, reverse=False):
151 if not setting_name:
152 raise Exception("You need to specify a configuration setting "
153 "to sort by.")
154 self._ensureUnlocked()
155 self._ensureUnloaded()
156 self._pages = SettingSortIterator(self._pages, setting_name, reverse)
157 self._has_sorter = True
158 return self
159
160 def reset(self):
161 self._ensureUnlocked()
162 self._unload()
163 return self
164
165 @property
166 def _is_loaded(self):
167 return self._cache is not None
168
169 @property
170 def _has_more(self):
171 if self._cache is None:
172 return False
173 if self._pagination_slicer:
174 return self._pagination_slicer.has_more
175 return False
176
177 def _simpleWrap(self, it_class, *args, **kwargs):
178 self._ensureUnlocked()
179 self._ensureUnloaded()
180 self._ensureSorter()
181 self._it = it_class(self._it, *args, **kwargs)
182 if self._pagination_slicer is None and it_class is SliceIterator:
183 self._pagination_slicer = self._it
184 self._pagination_slicer.current_page = self._current_page
185 return self
186
187 def _simpleNonSortedWrap(self, it_class, *args, **kwargs):
188 self._ensureUnlocked()
189 self._ensureUnloaded()
190 self._it = it_class(self._it, *args, **kwargs)
191 return self
192
193 def _lockIterator(self):
194 self._ensureUnlocked()
195 self._locked = True
196
197 def _ensureUnlocked(self):
198 if self._locked:
199 raise Exception(
200 "This page iterator has been locked and can't be modified.")
201
202 def _ensureUnloaded(self):
203 if self._cache:
204 raise Exception(
205 "This page iterator has already been iterated upon and "
206 "can't be modified anymore.")
207
208 def _ensureSorter(self):
209 if self._has_sorter:
210 return
211 self._it = DateSortIterator(self._it, reverse=True)
212 self._has_sorter = True
213
214 def _unload(self):
215 self._it = PageContentSourceIterator(self._source)
216 self._cache = None
217 self._paginationSlicer = None
218 self._has_sorter = False
219 self._next_page = None
220 self._prev_page = None
221
222 def _load(self):
223 if self._cache is not None:
224 return
225
226 if self._source.app.env.abort_source_use:
227 if self._current_page is not None:
228 logger.debug("Aborting iteration of '%s' from: %s." %
229 (self._source.name,
230 self._current_page.content_spec))
231 else:
232 logger.debug("Aborting iteration of '%s'." %
233 self._source.name)
234 raise AbortedSourceUseError()
235
236 self._ensureSorter()
237
238 tail_it = PaginationDataBuilderIterator(self._it, self._source.route)
239 self._cache = list(tail_it)
240
241 if (self._current_page is not None and
242 self._pagination_slicer is not None):
243 pn = [self._pagination_slicer.prev_page,
244 self._pagination_slicer.next_page]
245 pn_it = PaginationDataBuilderIterator(iter(pn),
246 self._source.route)
247 self._prev_page, self._next_page = (list(pn_it))
248
249 self._iter_event.fire(self)
250
251 def _debugRenderDoc(self):
252 return "Contains %d items" % len(self)
253
254
255 class SettingFilterIterator:
256 def __init__(self, it, fil_conf):
257 self.it = it
258 self.fil_conf = fil_conf
259 self._fil = None
260
261 def __iter__(self):
262 if self._fil is None:
263 self._fil = PaginationFilter()
264 self._fil.addClausesFromConfig(self.fil_conf)
265
266 for i in self.it:
267 if self._fil.pageMatches(i):
268 yield i
269
270
271 class HardCodedFilterIterator:
272 def __init__(self, it, fil):
273 self.it = it
274 self._fil = fil
275
276 def __iter__(self):
277 for i in self.it:
278 if self._fil.pageMatches(i):
279 yield i
280
281
282 class SliceIterator:
283 def __init__(self, it, offset=0, limit=-1):
284 self.it = it
285 self.offset = offset
286 self.limit = limit
287 self.current_page = None
288 self.has_more = False
289 self.inner_count = -1
290 self.next_page = None
291 self.prev_page = None
292 self._cache = None
293
294 def __iter__(self):
295 if self._cache is None:
296 inner_list = list(self.it)
297 self.inner_count = len(inner_list)
298
299 if self.limit > 0:
300 self.has_more = self.inner_count > (self.offset + self.limit)
301 self._cache = inner_list[self.offset:self.offset + self.limit]
302 else:
303 self.has_more = False
304 self._cache = inner_list[self.offset:]
305
306 if self.current_page:
307 try:
308 idx = inner_list.index(self.current_page)
309 except ValueError:
310 idx = -1
311 if idx >= 0:
312 if idx < self.inner_count - 1:
313 self.next_page = inner_list[idx + 1]
314 if idx > 0:
315 self.prev_page = inner_list[idx - 1]
316
317 return iter(self._cache)
318
319
320 class SettingSortIterator:
321 def __init__(self, it, name, reverse=False):
322 self.it = it
323 self.name = name
324 self.reverse = reverse
325
326 def __iter__(self):
327 return iter(sorted(self.it, key=self._key_getter,
328 reverse=self.reverse))
329
330 def _key_getter(self, item):
331 key = item.config.get(item)
332 if key is None:
333 return 0
334 return key
335
336
337 class DateSortIterator:
338 def __init__(self, it, reverse=True):
339 self.it = it
340 self.reverse = reverse
341
342 def __iter__(self):
343 return iter(sorted(self.it,
344 key=lambda x: x.datetime, reverse=self.reverse))
345
346
347 class PageContentSourceIterator:
348 def __init__(self, source):
349 self.source = source
350
351 # This is to permit recursive traversal of the
352 # iterator chain. It acts as the end.
353 self.it = None
354
355 def __iter__(self):
356 source = self.source
357 app = source.app
358 for item in source.getAllContents():
359 yield app.getPage(source, item)
360
361
362 class PaginationDataBuilderIterator:
363 def __init__(self, it, route):
364 self.it = it
365 self.route = route
366
367 def __iter__(self):
368 for page in self.it:
369 if page is not None:
370 yield PaginationData(page)
371 else:
372 yield None
373