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