Source code for flask_pushrod.resolver

from past.builtins import basestring
from past.builtins import long
from flask import current_app, request as current_request
from werkzeug.wrappers import BaseResponse

from . import renderers as _renderers, normalizers
from .renderers import RendererNotFound, UnrenderedResponse

from functools import wraps
from collections import defaultdict

import logging

import datetime

from types import GeneratorType

NoneType = type(None)

[docs]class Pushrod(object): """ The main resolver class for Pushrod. :param renderers: A tuple of renderers that are registered immediately (can also be strings, which are currently expanded to flask.ext.pushrod.renderers.%s_renderer) """ #: The query string argument checked for an explicit renderer (to override header-based content type negotiation). #: #: .. note:: #: This is set on the class level, not the instance level. format_arg_name = "format" @property def logger(self): """ Gets the logger to use, mainly for internal use. The current resolution order looks as follows: - :attr:`self.app <app>` - :data:`flask.current_app` - :mod:`logging` """ if self.app: return self.app.logger elif current_app: return current_app.logger else: return logging def __init__(self, app=None, renderers=('json', 'jinja2',), default_renderer='html'): #: The renderers keyed by MIME type. self.mime_type_renderers = {} #: The renderers keyed by output format name (such as html). self.named_renderers = {} #: Hooks for overriding a class' normalizer, even if they explicitly define one. #: #: All items should be lists of callables. All values default to an empty list. self.normalizer_overrides = defaultdict(lambda: []) #: Hooks for providing a class with a fallback normalizer, which is called only if it doesn't define one. All items should be callables. self.normalizers = { basestring: normalizers.normalize_basestring, str: normalizers.normalize_basestring, list: normalizers.normalize_iterable, tuple: normalizers.normalize_iterable, GeneratorType: normalizers.normalize_iterable, dict: normalizers.normalize_dict, int: normalizers.normalize_int, long: normalizers.normalize_int, float: normalizers.normalize_float, bool: normalizers.normalize_bool, NoneType: normalizers.normalize_none, datetime.datetime: normalizers.normalize_basestring, datetime.date: normalizers.normalize_basestring, datetime.time: normalizers.normalize_basestring, } #: The current app, only set from the constructor, not if using :meth:`init_app`. self.app = app or None for renderer in renderers: if isinstance(renderer, basestring): renderer = getattr(_renderers, '%s_renderer' % renderer) self.register_renderer(renderer) if isinstance(default_renderer, basestring): if default_renderer in self.named_renderers: default_renderer = self.named_renderers[default_renderer] else: self.logger.warning(u"Attempted to set the unrenderable format '%s' as the default format, disabling", default_renderer) default_renderer = None self.default_renderer = default_renderer if app: self.init_app(app)
[docs] def init_app(self, app): """ Registers the Pushrod resolver with the Flask app (can also be done by passing the app to the constructor). """ app.extensions['pushrod'] = self
[docs] def register_renderer(self, renderer, default=False): """ Registers a renderer with the Pushrod resolver (can also be done by passing the renderer to the constructor). """ if not (hasattr(renderer, '_is_pushrod_renderer') and renderer._is_pushrod_renderer): raise TypeError(u'Got passed an invalid renderer') for name in renderer.renderer_names: self.named_renderers[name] = renderer for mime_type in renderer.renderer_mime_types: self.mime_type_renderers[mime_type] = renderer
[docs] def get_renderers_for_request(self, request=None): """ Inspects a Flask :class:`~flask.Request` for hints regarding what renderer to use. This is found out by first looking in the query string argument named after :attr:`~format_arg_name` (``format`` by default), and then matching the contents of the Accept:-header. If nothing is found anyway, then :attr:`~default_renderer` is used. .. note:: If the query string argument is specified but doesn't exist (or fails), then the request fails immediately, without trying the other methods. :param request: The request to be inspected (defaults to :obj:`flask.request`) :returns: List of matching renderers, in order of user preference """ if request is None: request = current_request if self.format_arg_name in request.args: renderer_name = request.args[self.format_arg_name] if renderer_name in self.named_renderers: return [self.named_renderers[renderer_name]] else: return [] try: matching_renderers = [self.mime_type_renderers[mime_type] for mime_type in request.accept_mimetypes.itervalues() if mime_type in self.mime_type_renderers] except: matching_renderers = [self.mime_type_renderers[mime_type] for mime_type in request.accept_mimetypes.values() if mime_type in self.mime_type_renderers] if self.default_renderer: matching_renderers.append(self.default_renderer) return matching_renderers
[docs] def render_response(self, response, renderer=None, renderer_kwargs=None): """ Renders an unrendered response (a bare value, a (response, status, headers)-:obj:`tuple`, or an :class:`~flask.ext.pushrod.renderers.UnrenderedResponse` object). :throws RendererNotFound: If a usable renderer could not be found (explicit renderer argument points to an invalid render, or no acceptable mime types can be used as targets and there is no default renderer) :param response: The response to render :param renderer: The renderer(s) to use (defaults to using :meth:`get_renderer_for_request`) :param renderer_kwargs: Any extra arguments to pass to the renderer .. note:: For convenience, a bare string (:obj:`unicode`, :obj:`str`, or any other :obj:`basestring` derivative), or a derivative of :class:`werkzeug.wrappers.BaseResponse` (such as :class:`flask.Response`) is passed through unchanged. .. note:: A renderer may mark itself as unable to render a specific response by returning :obj:`None`, in which case the next possible renderer is attempted. """ if renderer: if hasattr(renderer, "__iter__"): renderers = renderer else: renderers = [renderer] else: renderers = self.get_renderers_for_request() if renderer_kwargs is None: renderer_kwargs = {} if isinstance(response, tuple): response, status, headers = response else: status = headers = None if isinstance(response, (basestring, BaseResponse)): return response if not isinstance(response, UnrenderedResponse): response = UnrenderedResponse(response, status, headers) for renderer in renderers: rendered = renderer(response, **renderer_kwargs) if rendered is not NotImplemented: return rendered raise RendererNotFound()
[docs] def normalize(self, obj): """ Runs an object through the normalizer mechanism, with the goal of producing a value consisting only of "native types" (:obj:`unicode`, :obj:`int`, :obj:`long`, :obj:`float`, :obj:`dict`, :obj:`list`, etc). The resolution order looks like this: - Loop through :attr:`self.normalizer_overrides[type(obj)] <normalizer_overrides>` (taking parent classes into account), should be a callable taking (obj, pushrod), falls through on :obj:`NotImplemented` - :attr:`self.normalizers[type(obj)] <normalizers>` (taking parent classes into account), should be a callable taking (obj, pushrod), falls through on :obj:`NotImplemented` See :ref:`bundled-normalizers` for all default normalizers. :param obj: The object to normalize. """ for cls in type(obj).__mro__: for override in self.normalizer_overrides[cls]: attempt = override(obj, self) if attempt is not NotImplemented: return attempt attempt = normalizers.normalize_object(obj, self) if attempt is not NotImplemented: return attempt for cls in type(obj).__mro__: if cls in self.normalizers: attempt = self.normalizers[cls](obj, self) if attempt is not NotImplemented: return attempt return NotImplemented
[docs]def pushrod_view(**renderer_kwargs): """ Decorator that wraps view functions and renders their responses through :meth:`flask.ext.pushrod.Pushrod.render_response`. .. note:: Views should only return :obj:`dicts <dict>` or a type that :meth:`normalizes <Pushrod.normalize>` down to :obj:`dicts <dict>`. :param renderer_kwargs: Any extra arguments to pass to the renderer """ def decorator(f): @wraps(f) def wrapper(*view_args, **view_kwargs): response = f(*view_args, **view_kwargs) return current_app.extensions['pushrod'].render_response(response, renderer_kwargs=renderer_kwargs) return wrapper return decorator