comparison piecrust/dataproviders/page_iterator.py @ 853:f070a4fc033c

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