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