comparison piecrust/serving/server.py @ 555:daf8df5ade7d

serve: Refactor the server to make pieces usable by the debugging middleware.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 12 Aug 2015 23:04:46 -0700
parents 155c7e20414f
children 657384f08ca3
comparison
equal deleted inserted replaced
554:155c7e20414f 555:daf8df5ade7d
8 from werkzeug.exceptions import ( 8 from werkzeug.exceptions import (
9 NotFound, MethodNotAllowed, InternalServerError, HTTPException) 9 NotFound, MethodNotAllowed, InternalServerError, HTTPException)
10 from werkzeug.wrappers import Request, Response 10 from werkzeug.wrappers import Request, Response
11 from jinja2 import FileSystemLoader, Environment 11 from jinja2 import FileSystemLoader, Environment
12 from piecrust import CACHE_DIR, RESOURCES_DIR 12 from piecrust import CACHE_DIR, RESOURCES_DIR
13 from piecrust.app import PieCrust 13 from piecrust.rendering import PageRenderingContext, render_page
14 from piecrust.rendering import QualifiedPage, PageRenderingContext, render_page 14 from piecrust.routing import RouteNotFoundError
15 from piecrust.serving.util import content_type_map, make_wrapped_file_response 15 from piecrust.serving.util import (
16 from piecrust.sources.base import MODE_PARSING 16 content_type_map, make_wrapped_file_response, get_requested_page,
17 from piecrust.uriutil import split_sub_uri 17 get_app_for_server)
18 from piecrust.sources.base import SourceNotFoundError
18 19
19 20
20 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
21 22
22 23
61 def get_description(self, environ=None): 62 def get_description(self, environ=None):
62 from werkzeug.utils import escape 63 from werkzeug.utils import escape
63 desc = '<p>' + self.description + '</p>' 64 desc = '<p>' + self.description + '</p>'
64 desc += '<p>' 65 desc += '<p>'
65 for nfe in self._nfes: 66 for nfe in self._nfes:
66 desc += '<li>' + escape(nfe.description) + '</li>' 67 desc += '<li>' + escape(str(nfe)) + '</li>'
67 desc += '</p>' 68 desc += '</p>'
68 return desc 69 return desc
69 70
70 71
71 class Server(object): 72 class Server(object):
105 response = self._try_serve_asset(environ, request) 106 response = self._try_serve_asset(environ, request)
106 if response is not None: 107 if response is not None:
107 return response 108 return response
108 109
109 # Create the app for this request. 110 # Create the app for this request.
110 app = PieCrust(root_dir=self.root_dir, debug=self.debug) 111 app = get_app_for_server(self.root_dir, debug=self.debug,
111 if self.sub_cache_dir: 112 sub_cache_dir=self.sub_cache_dir)
112 app._useSubCacheDir(self.sub_cache_dir)
113 app.config.set('site/root', '/')
114 app.config.set('server/is_serving', True)
115 if (app.config.get('site/enable_debug_info') and 113 if (app.config.get('site/enable_debug_info') and
116 self.enable_debug_info and 114 self.enable_debug_info and
117 '!debug' in request.args): 115 '!debug' in request.args):
118 app.config.set('site/show_debug_info', True) 116 app.config.set('site/show_debug_info', True)
119 117
166 return None 164 return None
167 165
168 return make_wrapped_file_response(environ, request, full_path) 166 return make_wrapped_file_response(environ, request, full_path)
169 167
170 def _try_serve_page(self, app, environ, request): 168 def _try_serve_page(self, app, environ, request):
171 # Try to find what matches the requested URL. 169 # Find a matching page.
172 req_path, page_num = split_sub_uri(app, request.path) 170 req_page = get_requested_page(app, request.path)
173 171
174 routes = find_routes(app.routes, req_path) 172 # If we haven't found any good match, report all the places we didn't
175 if len(routes) == 0: 173 # find it at.
176 raise RouteNotFoundError("Can't find route for: %s" % req_path) 174 qp = req_page.qualified_page
177 175 if qp is None:
178 rendered_page = None 176 msg = "Can't find path for '%s':" % request.path
179 not_found_errors = [] 177 raise MultipleNotFound(msg, req_page.not_found_errors)
180 for route, route_metadata in routes: 178
181 try: 179 # We have a page, let's try to render it.
182 logger.debug("Trying to render match from source '%s'." % 180 render_ctx = PageRenderingContext(qp,
183 route.source_name) 181 page_num=req_page.page_num,
184 rendered_page = self._try_render_page( 182 force_render=True)
185 app, route, route_metadata, page_num, req_path) 183 if qp.route.taxonomy_name is not None:
186 if rendered_page is not None: 184 taxonomy = app.getTaxonomy(qp.route.taxonomy_name)
187 break 185 tax_terms = qp.route.getTaxonomyTerms(qp.route_metadata)
188 except NotFound as nfe: 186 render_ctx.setTaxonomyFilter(tax_terms, needs_slugifier=True)
189 not_found_errors.append(nfe) 187
190 188 # See if this page is known to use sources. If that's the case,
191 # If we haven't found any good match, raise whatever exception we 189 # just don't use cached rendered segments for that page (but still
192 # first got. Otherwise, raise a generic exception. 190 # use them for pages that are included in it).
193 if rendered_page is None: 191 uri = qp.getUri()
194 msg = ("Can't find path for '%s', looked in sources: %s" % 192 entry = self._page_record.getEntry(uri, req_page.page_num)
195 (req_path, 193 if (qp.route.taxonomy_name is not None or entry is None or
196 ', '.join([r.source_name for r, _ in routes]))) 194 entry.used_source_names):
197 raise MultipleNotFound(msg, not_found_errors) 195 cache_key = '%s:%s' % (uri, req_page.page_num)
196 app.env.rendered_segments_repository.invalidate(cache_key)
197
198 # Render the page.
199 rendered_page = render_page(render_ctx)
200
201 # Check if this page is a taxonomy page that actually doesn't match
202 # anything.
203 if qp.route.taxonomy_name is not None:
204 paginator = rendered_page.data.get('pagination')
205 if (paginator and paginator.is_loaded and
206 len(paginator.items) == 0):
207 taxonomy = app.getTaxonomy(qp.route.taxonomy_name)
208 message = ("This URL matched a route for taxonomy '%s' but "
209 "no pages have been found to have it. This page "
210 "won't be generated by a bake." % taxonomy.name)
211 raise NotFound(message)
212
213 # Remember stuff for next time.
214 if entry is None:
215 entry = ServeRecordPageEntry(req_page.req_path, req_page.page_num)
216 self._page_record.addEntry(entry)
217 for p, pinfo in render_ctx.render_passes.items():
218 entry.used_source_names |= pinfo.used_source_names
198 219
199 # Start doing stuff. 220 # Start doing stuff.
200 page = rendered_page.page 221 page = rendered_page.page
201 rp_content = rendered_page.content 222 rp_content = rendered_page.content
202 223
253 "falling back to uncompressed.") 274 "falling back to uncompressed.")
254 response.set_data(rp_content) 275 response.set_data(rp_content)
255 276
256 return response 277 return response
257 278
258 def _try_render_page(self, app, route, route_metadata, page_num, req_path):
259 # Match the route to an actual factory.
260 taxonomy_info = None
261 source = app.getSource(route.source_name)
262 if route.taxonomy_name is None:
263 factory = source.findPageFactory(route_metadata, MODE_PARSING)
264 if factory is None:
265 raise NotFound("No path found for '%s' in source '%s'." %
266 (req_path, source.name))
267 else:
268 taxonomy = app.getTaxonomy(route.taxonomy_name)
269 tax_terms = route.getTaxonomyTerms(route_metadata)
270 taxonomy_info = (taxonomy, tax_terms)
271
272 tax_page_ref = taxonomy.getPageRef(source)
273 factory = tax_page_ref.getFactory()
274
275 # Build the page.
276 page = factory.buildPage()
277 # We force the rendering of the page because it could not have
278 # changed, but include pages that did change.
279 qp = QualifiedPage(page, route, route_metadata)
280 render_ctx = PageRenderingContext(qp,
281 page_num=page_num,
282 force_render=True)
283 if taxonomy_info is not None:
284 _, tax_terms = taxonomy_info
285 render_ctx.setTaxonomyFilter(tax_terms, needs_slugifier=True)
286
287 # See if this page is known to use sources. If that's the case,
288 # just don't use cached rendered segments for that page (but still
289 # use them for pages that are included in it).
290 uri = qp.getUri()
291 entry = self._page_record.getEntry(uri, page_num)
292 if (taxonomy_info is not None or entry is None or
293 entry.used_source_names):
294 cache_key = '%s:%s' % (uri, page_num)
295 app.env.rendered_segments_repository.invalidate(cache_key)
296
297 # Render the page.
298 rendered_page = render_page(render_ctx)
299
300 # Check if this page is a taxonomy page that actually doesn't match
301 # anything.
302 if taxonomy_info is not None:
303 paginator = rendered_page.data.get('pagination')
304 if (paginator and paginator.is_loaded and
305 len(paginator.items) == 0):
306 taxonomy = taxonomy_info[0]
307 message = ("This URL matched a route for taxonomy '%s' but "
308 "no pages have been found to have it. This page "
309 "won't be generated by a bake." % taxonomy.name)
310 raise NotFound(message)
311
312 # Remember stuff for next time.
313 if entry is None:
314 entry = ServeRecordPageEntry(req_path, page_num)
315 self._page_record.addEntry(entry)
316 for p, pinfo in render_ctx.render_passes.items():
317 entry.used_source_names |= pinfo.used_source_names
318
319 # Ok all good.
320 return rendered_page
321
322 def _handle_error(self, exception, environ, start_response): 279 def _handle_error(self, exception, environ, start_response):
323 code = 500 280 code = 500
324 if isinstance(exception, HTTPException): 281 if isinstance(exception, HTTPException):
325 code = exception.code 282 code = exception.code
326 283
352 inner_ex = exception.__context__ 309 inner_ex = exception.__context__
353 exception = inner_ex 310 exception = inner_ex
354 return desc 311 return desc
355 312
356 313
357 class RouteNotFoundError(Exception):
358 pass
359
360
361 class SourceNotFoundError(Exception):
362 pass
363
364
365 def find_routes(routes, uri):
366 res = []
367 tax_res = []
368 for route in routes:
369 metadata = route.matchUri(uri)
370 if metadata is not None:
371 if route.is_taxonomy_route:
372 tax_res.append((route, metadata))
373 else:
374 res.append((route, metadata))
375 return res + tax_res
376
377
378 class ErrorMessageLoader(FileSystemLoader): 314 class ErrorMessageLoader(FileSystemLoader):
379 def __init__(self): 315 def __init__(self):
380 base_dir = os.path.join(RESOURCES_DIR, 'messages') 316 base_dir = os.path.join(RESOURCES_DIR, 'messages')
381 super(ErrorMessageLoader, self).__init__(base_dir) 317 super(ErrorMessageLoader, self).__init__(base_dir)
382 318