Mercurial > piecrust2
comparison piecrust/rendering.py @ 852:4850f8c21b6e
core: Start of the big refactor for PieCrust 3.0.
* Everything is a `ContentSource`, including assets directories.
* Most content sources are subclasses of the base file-system source.
* A source is processed by a "pipeline", and there are 2 built-in pipelines,
one for assets and one for pages. The asset pipeline is vaguely functional,
but the page pipeline is completely broken right now.
* Rewrite the baking process as just running appropriate pipelines on each
content item. This should allow for better parallelization.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 17 May 2017 00:11:48 -0700 |
parents | dca51cd8147a |
children | f070a4fc033c |
comparison
equal
deleted
inserted
replaced
851:2c7e57d80bba | 852:4850f8c21b6e |
---|---|
1 import re | 1 import re |
2 import os.path | 2 import os.path |
3 import copy | 3 import copy |
4 import logging | 4 import logging |
5 from werkzeug.utils import cached_property | |
6 from piecrust.data.builder import ( | 5 from piecrust.data.builder import ( |
7 DataBuildingContext, build_page_data, build_layout_data) | 6 DataBuildingContext, build_page_data, add_layout_data) |
8 from piecrust.data.filters import ( | |
9 PaginationFilter, SettingFilterClause, page_value_accessor) | |
10 from piecrust.fastpickle import _pickle_object, _unpickle_object | 7 from piecrust.fastpickle import _pickle_object, _unpickle_object |
11 from piecrust.sources.base import PageSource | 8 from piecrust.sources.base import ContentSource |
12 from piecrust.templating.base import TemplateNotFoundError, TemplatingError | 9 from piecrust.templating.base import TemplateNotFoundError, TemplatingError |
13 | 10 |
14 | 11 |
15 logger = logging.getLogger(__name__) | 12 logger = logging.getLogger(__name__) |
16 | 13 |
17 | 14 |
18 content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$', | 15 content_abstract_re = re.compile(r'^<!--\s*(more|(page)?break)\s*-->\s*$', |
19 re.MULTILINE) | 16 re.MULTILINE) |
20 | 17 |
21 | 18 |
22 class PageRenderingError(Exception): | 19 class RenderingError(Exception): |
23 pass | 20 pass |
24 | 21 |
25 | 22 |
26 class TemplateEngineNotFound(Exception): | 23 class TemplateEngineNotFound(Exception): |
27 pass | 24 pass |
28 | |
29 | |
30 class QualifiedPage(object): | |
31 def __init__(self, page, route, route_metadata): | |
32 self.page = page | |
33 self.route = route | |
34 self.route_metadata = route_metadata | |
35 | |
36 def getUri(self, sub_num=1): | |
37 return self.route.getUri(self.route_metadata, sub_num=sub_num) | |
38 | |
39 def __getattr__(self, name): | |
40 return getattr(self.page, name) | |
41 | 25 |
42 | 26 |
43 class RenderedSegments(object): | 27 class RenderedSegments(object): |
44 def __init__(self, segments, render_pass_info): | 28 def __init__(self, segments, render_pass_info): |
45 self.segments = segments | 29 self.segments = segments |
51 self.content = content | 35 self.content = content |
52 self.render_pass_info = render_pass_info | 36 self.render_pass_info = render_pass_info |
53 | 37 |
54 | 38 |
55 class RenderedPage(object): | 39 class RenderedPage(object): |
56 def __init__(self, page, uri, num=1): | 40 def __init__(self, qualified_page): |
57 self.page = page | 41 self.qualified_page = qualified_page |
58 self.uri = uri | |
59 self.num = num | |
60 self.data = None | 42 self.data = None |
61 self.content = None | 43 self.content = None |
62 self.render_info = [None, None] | 44 self.render_info = [None, None] |
63 | 45 |
64 @property | 46 @property |
65 def app(self): | 47 def app(self): |
66 return self.page.app | 48 return self.qualified_page.app |
67 | 49 |
68 def copyRenderInfo(self): | 50 def copyRenderInfo(self): |
69 return copy.deepcopy(self.render_info) | 51 return copy.deepcopy(self.render_info) |
70 | 52 |
71 | 53 |
92 if create_if_missing: | 74 if create_if_missing: |
93 return self._custom_info.setdefault(key, default) | 75 return self._custom_info.setdefault(key, default) |
94 return self._custom_info.get(key, default) | 76 return self._custom_info.get(key, default) |
95 | 77 |
96 | 78 |
97 class PageRenderingContext(object): | 79 class RenderingContext(object): |
98 def __init__(self, qualified_page, page_num=1, | 80 def __init__(self, qualified_page, force_render=False): |
99 force_render=False, is_from_request=False): | 81 self.qualified_page = qualified_page |
100 self.page = qualified_page | |
101 self.page_num = page_num | |
102 self.force_render = force_render | 82 self.force_render = force_render |
103 self.is_from_request = is_from_request | |
104 self.pagination_source = None | 83 self.pagination_source = None |
105 self.pagination_filter = None | 84 self.pagination_filter = None |
106 self.custom_data = {} | 85 self.custom_data = {} |
107 self.render_passes = [None, None] # Same length as RENDER_PASSES | 86 self.render_passes = [None, None] # Same length as RENDER_PASSES |
108 self._current_pass = PASS_NONE | 87 self._current_pass = PASS_NONE |
109 | 88 |
110 @property | 89 @property |
111 def app(self): | 90 def app(self): |
112 return self.page.app | 91 return self.qualified_page.app |
113 | |
114 @property | |
115 def source_metadata(self): | |
116 return self.page.source_metadata | |
117 | |
118 @cached_property | |
119 def uri(self): | |
120 return self.page.getUri(self.page_num) | |
121 | 92 |
122 @property | 93 @property |
123 def current_pass_info(self): | 94 def current_pass_info(self): |
124 if self._current_pass != PASS_NONE: | 95 if self._current_pass != PASS_NONE: |
125 return self.render_passes[self._current_pass] | 96 return self.render_passes[self._current_pass] |
140 pass_info.pagination_has_more = paginator.has_more | 111 pass_info.pagination_has_more = paginator.has_more |
141 self.addUsedSource(paginator._source) | 112 self.addUsedSource(paginator._source) |
142 | 113 |
143 def addUsedSource(self, source): | 114 def addUsedSource(self, source): |
144 self._raiseIfNoCurrentPass() | 115 self._raiseIfNoCurrentPass() |
145 if isinstance(source, PageSource): | 116 if isinstance(source, ContentSource): |
146 pass_info = self.current_pass_info | 117 pass_info = self.current_pass_info |
147 pass_info.used_source_names.add(source.name) | 118 pass_info.used_source_names.add(source.name) |
148 | 119 |
149 def _raiseIfNoCurrentPass(self): | 120 def _raiseIfNoCurrentPass(self): |
150 if self._current_pass == PASS_NONE: | 121 if self._current_pass == PASS_NONE: |
151 raise Exception("No rendering pass is currently active.") | 122 raise Exception("No rendering pass is currently active.") |
152 | 123 |
153 | 124 |
125 class RenderingContextStack(object): | |
126 def __init__(self): | |
127 self._ctx_stack = [] | |
128 | |
129 @property | |
130 def current_ctx(self): | |
131 if len(self._ctx_stack) == 0: | |
132 return None | |
133 return self._ctx_stack[-1] | |
134 | |
135 @property | |
136 def is_main_ctx(self): | |
137 return len(self._ctx_stack) == 1 | |
138 | |
139 def hasPage(self, page): | |
140 for ei in self._ctx_stack: | |
141 if ei.qualified_page.page == page: | |
142 return True | |
143 return False | |
144 | |
145 def pushCtx(self, render_ctx): | |
146 for ctx in self._ctx_stack: | |
147 if ctx.qualified_page.page == render_ctx.qualified_page.page: | |
148 raise Exception("Loop detected during rendering!") | |
149 self._ctx_stack.append(render_ctx) | |
150 | |
151 def popCtx(self): | |
152 del self._ctx_stack[-1] | |
153 | |
154 def clear(self): | |
155 self._ctx_stack = [] | |
156 | |
157 | |
154 def render_page(ctx): | 158 def render_page(ctx): |
155 eis = ctx.app.env.exec_info_stack | 159 env = ctx.app.env |
156 eis.pushPage(ctx.page, ctx) | 160 |
161 stack = env.render_ctx_stack | |
162 stack.pushCtx(ctx) | |
163 | |
164 qpage = ctx.qualified_page | |
165 | |
157 try: | 166 try: |
158 # Build the data for both segment and layout rendering. | 167 # Build the data for both segment and layout rendering. |
159 with ctx.app.env.timerScope("BuildRenderData"): | 168 with env.timerScope("BuildRenderData"): |
160 page_data = _build_render_data(ctx) | 169 page_data = _build_render_data(ctx) |
161 | 170 |
162 # Render content segments. | 171 # Render content segments. |
163 ctx.setCurrentPass(PASS_FORMATTING) | 172 ctx.setCurrentPass(PASS_FORMATTING) |
164 repo = ctx.app.env.rendered_segments_repository | 173 repo = env.rendered_segments_repository |
165 save_to_fs = True | 174 save_to_fs = True |
166 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: | 175 if env.fs_cache_only_for_main_page and not stack.is_main_ctx: |
167 save_to_fs = False | 176 save_to_fs = False |
168 with ctx.app.env.timerScope("PageRenderSegments"): | 177 with env.timerScope("PageRenderSegments"): |
169 if repo and not ctx.force_render: | 178 if repo is not None and not ctx.force_render: |
170 render_result = repo.get( | 179 render_result = repo.get( |
171 ctx.uri, | 180 qpage.uri, |
172 lambda: _do_render_page_segments(ctx.page, page_data), | 181 lambda: _do_render_page_segments(ctx, page_data), |
173 fs_cache_time=ctx.page.path_mtime, | 182 fs_cache_time=qpage.page.content_mtime, |
174 save_to_fs=save_to_fs) | 183 save_to_fs=save_to_fs) |
175 else: | 184 else: |
176 render_result = _do_render_page_segments(ctx.page, page_data) | 185 render_result = _do_render_page_segments(ctx, page_data) |
177 if repo: | 186 if repo: |
178 repo.put(ctx.uri, render_result, save_to_fs) | 187 repo.put(qpage.uri, render_result, save_to_fs) |
179 | 188 |
180 # Render layout. | 189 # Render layout. |
181 page = ctx.page | |
182 ctx.setCurrentPass(PASS_RENDERING) | 190 ctx.setCurrentPass(PASS_RENDERING) |
183 layout_name = page.config.get('layout') | 191 layout_name = qpage.page.config.get('layout') |
184 if layout_name is None: | 192 if layout_name is None: |
185 layout_name = page.source.config.get('default_layout', 'default') | 193 layout_name = qpage.page.source.config.get( |
194 'default_layout', 'default') | |
186 null_names = ['', 'none', 'nil'] | 195 null_names = ['', 'none', 'nil'] |
187 if layout_name not in null_names: | 196 if layout_name not in null_names: |
188 with ctx.app.env.timerScope("BuildRenderData"): | 197 with ctx.app.env.timerScope("BuildRenderData"): |
189 build_layout_data(page, page_data, render_result['segments']) | 198 add_layout_data(page_data, render_result['segments']) |
190 | 199 |
191 with ctx.app.env.timerScope("PageRenderLayout"): | 200 with ctx.app.env.timerScope("PageRenderLayout"): |
192 layout_result = _do_render_layout(layout_name, page, page_data) | 201 layout_result = _do_render_layout( |
202 layout_name, qpage, page_data) | |
193 else: | 203 else: |
194 layout_result = { | 204 layout_result = { |
195 'content': render_result['segments']['content'], | 205 'content': render_result['segments']['content'], |
196 'pass_info': None} | 206 'pass_info': None} |
197 | 207 |
198 rp = RenderedPage(page, ctx.uri, ctx.page_num) | 208 rp = RenderedPage(qpage) |
199 rp.data = page_data | 209 rp.data = page_data |
200 rp.content = layout_result['content'] | 210 rp.content = layout_result['content'] |
201 rp.render_info[PASS_FORMATTING] = _unpickle_object( | 211 rp.render_info[PASS_FORMATTING] = _unpickle_object( |
202 render_result['pass_info']) | 212 render_result['pass_info']) |
203 if layout_result['pass_info'] is not None: | 213 if layout_result['pass_info'] is not None: |
204 rp.render_info[PASS_RENDERING] = _unpickle_object( | 214 rp.render_info[PASS_RENDERING] = _unpickle_object( |
205 layout_result['pass_info']) | 215 layout_result['pass_info']) |
206 return rp | 216 return rp |
217 | |
207 except Exception as ex: | 218 except Exception as ex: |
208 if ctx.app.debug: | 219 if ctx.app.debug: |
209 raise | 220 raise |
210 logger.exception(ex) | 221 logger.exception(ex) |
211 page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir) | 222 page_rel_path = os.path.relpath(ctx.page.path, ctx.app.root_dir) |
212 raise Exception("Error rendering page: %s" % page_rel_path) from ex | 223 raise Exception("Error rendering page: %s" % page_rel_path) from ex |
224 | |
213 finally: | 225 finally: |
214 ctx.setCurrentPass(PASS_NONE) | 226 ctx.setCurrentPass(PASS_NONE) |
215 eis.popPage() | 227 stack.popCtx() |
216 | 228 |
217 | 229 |
218 def render_page_segments(ctx): | 230 def render_page_segments(ctx): |
219 eis = ctx.app.env.exec_info_stack | 231 env = ctx.app.env |
220 eis.pushPage(ctx.page, ctx) | 232 |
233 stack = env.render_ctx_stack | |
234 stack.pushCtx(ctx) | |
235 | |
236 qpage = ctx.qualified_page | |
237 | |
221 try: | 238 try: |
222 ctx.setCurrentPass(PASS_FORMATTING) | 239 ctx.setCurrentPass(PASS_FORMATTING) |
223 repo = ctx.app.env.rendered_segments_repository | 240 repo = ctx.app.env.rendered_segments_repository |
224 save_to_fs = True | 241 save_to_fs = True |
225 if ctx.app.env.fs_cache_only_for_main_page and not eis.is_main_page: | 242 if ctx.app.env.fs_cache_only_for_main_page and not stack.is_main_ctx: |
226 save_to_fs = False | 243 save_to_fs = False |
227 with ctx.app.env.timerScope("PageRenderSegments"): | 244 with ctx.app.env.timerScope("PageRenderSegments"): |
228 if repo and not ctx.force_render: | 245 if repo is not None and not ctx.force_render: |
229 render_result = repo.get( | 246 render_result = repo.get( |
230 ctx.uri, | 247 qpage.uri, |
231 lambda: _do_render_page_segments_from_ctx(ctx), | 248 lambda: _do_render_page_segments_from_ctx(ctx), |
232 fs_cache_time=ctx.page.path_mtime, | 249 fs_cache_time=qpage.page.content_mtime, |
233 save_to_fs=save_to_fs) | 250 save_to_fs=save_to_fs) |
234 else: | 251 else: |
235 render_result = _do_render_page_segments_from_ctx(ctx) | 252 render_result = _do_render_page_segments_from_ctx(ctx) |
236 if repo: | 253 if repo: |
237 repo.put(ctx.uri, render_result, save_to_fs) | 254 repo.put(qpage.uri, render_result, save_to_fs) |
238 finally: | 255 finally: |
239 ctx.setCurrentPass(PASS_NONE) | 256 ctx.setCurrentPass(PASS_NONE) |
240 eis.popPage() | 257 stack.popCtx() |
241 | 258 |
242 rs = RenderedSegments( | 259 rs = RenderedSegments( |
243 render_result['segments'], | 260 render_result['segments'], |
244 _unpickle_object(render_result['pass_info'])) | 261 _unpickle_object(render_result['pass_info'])) |
245 return rs | 262 return rs |
246 | 263 |
247 | 264 |
248 def _build_render_data(ctx): | 265 def _build_render_data(ctx): |
249 with ctx.app.env.timerScope("PageDataBuild"): | 266 with ctx.app.env.timerScope("PageDataBuild"): |
250 data_ctx = DataBuildingContext(ctx.page, page_num=ctx.page_num) | 267 data_ctx = DataBuildingContext(ctx.qualified_page) |
251 data_ctx.pagination_source = ctx.pagination_source | 268 data_ctx.pagination_source = ctx.pagination_source |
252 data_ctx.pagination_filter = ctx.pagination_filter | 269 data_ctx.pagination_filter = ctx.pagination_filter |
253 page_data = build_page_data(data_ctx) | 270 page_data = build_page_data(data_ctx) |
254 if ctx.custom_data: | 271 if ctx.custom_data: |
255 page_data._appendMapping(ctx.custom_data) | 272 page_data._appendMapping(ctx.custom_data) |
256 return page_data | 273 return page_data |
257 | 274 |
258 | 275 |
259 def _do_render_page_segments_from_ctx(ctx): | 276 def _do_render_page_segments_from_ctx(ctx): |
260 page_data = _build_render_data(ctx) | 277 page_data = _build_render_data(ctx) |
261 return _do_render_page_segments(ctx.page, page_data) | 278 return _do_render_page_segments(ctx, page_data) |
262 | 279 |
263 | 280 |
264 def _do_render_page_segments(page, page_data): | 281 def _do_render_page_segments(ctx, page_data): |
282 page = ctx.qualified_page.page | |
265 app = page.app | 283 app = page.app |
266 | |
267 cpi = app.env.exec_info_stack.current_page_info | |
268 assert cpi is not None | |
269 assert cpi.page == page | |
270 | 284 |
271 engine_name = page.config.get('template_engine') | 285 engine_name = page.config.get('template_engine') |
272 format_name = page.config.get('format') | 286 format_name = page.config.get('format') |
273 | 287 |
274 engine = get_template_engine(app, engine_name) | 288 engine = get_template_engine(app, engine_name) |
280 part_format = seg_part.fmt or format_name | 294 part_format = seg_part.fmt or format_name |
281 try: | 295 try: |
282 with app.env.timerScope( | 296 with app.env.timerScope( |
283 engine.__class__.__name__ + '_segment'): | 297 engine.__class__.__name__ + '_segment'): |
284 part_text = engine.renderSegmentPart( | 298 part_text = engine.renderSegmentPart( |
285 page.path, seg_part, page_data) | 299 page.path, seg_part, page_data) |
286 except TemplatingError as err: | 300 except TemplatingError as err: |
287 err.lineno += seg_part.line | 301 err.lineno += seg_part.line |
288 raise err | 302 raise err |
289 | 303 |
290 part_text = format_text(app, part_format, part_text) | 304 part_text = format_text(app, part_format, part_text) |
296 if m: | 310 if m: |
297 offset = m.start() | 311 offset = m.start() |
298 content_abstract = seg_text[:offset] | 312 content_abstract = seg_text[:offset] |
299 formatted_segments['content.abstract'] = content_abstract | 313 formatted_segments['content.abstract'] = content_abstract |
300 | 314 |
301 pass_info = cpi.render_ctx.render_passes[PASS_FORMATTING] | 315 pass_info = ctx.render_passes[PASS_FORMATTING] |
302 res = { | 316 res = { |
303 'segments': formatted_segments, | 317 'segments': formatted_segments, |
304 'pass_info': _pickle_object(pass_info)} | 318 'pass_info': _pickle_object(pass_info)} |
305 return res | 319 return res |
306 | 320 |
307 | 321 |
308 def _do_render_layout(layout_name, page, layout_data): | 322 def _do_render_layout(layout_name, page, layout_data): |
309 cpi = page.app.env.exec_info_stack.current_page_info | 323 cpi = page.app.env.exec_info_stack.current_page_info |