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