cleaned Request/Response/mixins to have similar interface

This commit is contained in:
Sébastien Piquemal 2012-02-07 15:38:54 +02:00
parent ca96b4523b
commit 21292d31e7
5 changed files with 145 additions and 167 deletions

View File

@ -12,7 +12,7 @@ from djangorestframework import status
from djangorestframework.renderers import BaseRenderer from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ImmediateResponse from djangorestframework.response import Response, ImmediateResponse
from djangorestframework.request import request_class_factory from djangorestframework.request import Request
from djangorestframework.utils import as_tuple, allowed_methods from djangorestframework.utils import as_tuple, allowed_methods
@ -32,7 +32,6 @@ __all__ = (
'ListModelMixin' 'ListModelMixin'
) )
#TODO: In RequestMixin and ResponseMixin : get_response_class/get_request_class are a bit ugly. Do we even want to be able to set the parameters on the view ?
########## Request Mixin ########## ########## Request Mixin ##########
@ -41,39 +40,43 @@ class RequestMixin(object):
`Mixin` class to enhance API of Django's standard `request`. `Mixin` class to enhance API of Django's standard `request`.
""" """
_USE_FORM_OVERLOADING = True parser_classes = ()
_METHOD_PARAM = '_method'
_CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content'
parsers = ()
""" """
The set of parsers that the request can handle. The set of parsers that the view can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module. Should be a tuple/list of classes as described in the :mod:`parsers` module.
""" """
def get_request_class(self): request_class = Request
""" """
Returns a subclass of Django's `HttpRequest` with a richer API, The class to use as a wrapper for the original request object.
as described in :mod:`request`.
""" """
if not hasattr(self, '_request_class'):
self._request_class = request_class_factory(self.request)
self._request_class._USE_FORM_OVERLOADING = self._USE_FORM_OVERLOADING
self._request_class._METHOD_PARAM = self._METHOD_PARAM
self._request_class._CONTENTTYPE_PARAM = self._CONTENTTYPE_PARAM
self._request_class._CONTENT_PARAM = self._CONTENT_PARAM
self._request_class.parsers = self.parsers
return self._request_class
def get_request(self): def get_parsers(self):
""" """
Returns a custom request instance, with data and attributes copied from the Instantiates and returns the list of parsers that will be used by the request
original request. to parse its content.
""" """
request_class = self.get_request_class() if not hasattr(self, '_parsers'):
return request_class(self.request) self._parsers = [r(self) for r in self.parser_classes]
return self._parsers
def prepare_request(self, request):
"""
Prepares the request for the request cycle. Returns a custom request instance,
with data and attributes copied from the original request.
"""
parsers = self.get_parsers()
request = self.request_class(request, parsers=parsers)
self.request = request
return request
@property
def _parsed_media_types(self):
"""
Return a list of all the media types that this view can parse.
"""
return [p.media_type for p in self.parser_classes]
########## ResponseMixin ########## ########## ResponseMixin ##########
@ -105,8 +108,8 @@ class ResponseMixin(object):
def prepare_response(self, response): def prepare_response(self, response):
""" """
Prepares the response for the response cycle. This has no effect if the Prepares the response for the response cycle, and returns the prepared response.
response is not an instance of :class:`response.Response`. This has no effect if the response is not an instance of :class:`response.Response`.
""" """
if hasattr(response, 'request') and response.request is None: if hasattr(response, 'request') and response.request is None:
response.request = self.request response.request = self.request
@ -124,6 +127,17 @@ class ResponseMixin(object):
self.response = response self.response = response
return response return response
@property
def headers(self):
"""
Dictionary of headers to set on the response.
This is useful when the response doesn't exist yet, but you
want to memorize some headers to set on it when it will exist.
"""
if not hasattr(self, '_headers'):
self._headers = {}
return self._headers
@property @property
def _rendered_media_types(self): def _rendered_media_types(self):
""" """
@ -138,24 +152,6 @@ class ResponseMixin(object):
""" """
return [renderer.format for renderer in self.get_renderers()] return [renderer.format for renderer in self.get_renderers()]
@property
def _default_renderer(self):
"""
Return the view's default renderer class.
"""
return self.get_renderers()[0]
@property
def headers(self):
"""
Dictionary of headers to set on the response.
This is useful when the response doesn't exist yet, but you
want to memorize some headers to set on it when it will exist.
"""
if not hasattr(self, '_headers'):
self._headers = {}
return self._headers
########## Auth Mixin ########## ########## Auth Mixin ##########

View File

@ -43,7 +43,7 @@ class BaseParser(object):
media_type = None media_type = None
def __init__(self, view): def __init__(self, view=None):
""" """
Initialize the parser with the ``View`` instance as state, Initialize the parser with the ``View`` instance as state,
in case the parser needs to access any metadata on the :obj:`View` object. in case the parser needs to access any metadata on the :obj:`View` object.
@ -167,10 +167,9 @@ class MultiPartParser(BaseParser):
`data` will be a :class:`QueryDict` containing all the form parameters. `data` will be a :class:`QueryDict` containing all the form parameters.
`files` will be a :class:`QueryDict` containing all the form files. `files` will be a :class:`QueryDict` containing all the form files.
""" """
# TODO: now self.view is in fact request, but should disappear ... upload_handlers = self.view.request._get_upload_handlers()
upload_handlers = self.view._get_upload_handlers()
try: try:
django_parser = DjangoMultiPartParser(self.view.META, stream, upload_handlers) django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
except MultiPartParserError, exc: except MultiPartParserError, exc:
raise ImmediateResponse( raise ImmediateResponse(
content={'detail': 'multipart parse error - %s' % unicode(exc)}, content={'detail': 'multipart parse error - %s' % unicode(exc)},

View File

@ -1,10 +1,10 @@
""" """
The :mod:`request` module provides a :class:`Request` class that can be used The :mod:`request` module provides a :class:`Request` class that can be used
to enhance the standard `request` object received in all the views. to wrap the standard `request` object received in all the views, and upgrade its API.
This enhanced request object offers the following : The wrapped request then offer the following :
- content automatically parsed according to `Content-Type` header, and available as :meth:`request.DATA<Request.DATA>` - content automatically parsed according to `Content-Type` header, and available as :meth:`.DATA<Request.DATA>`
- full support of PUT method, including support for file uploads - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - form overloading of HTTP method, content type and content
""" """
@ -22,21 +22,9 @@ from StringIO import StringIO
__all__ = ('Request',) __all__ = ('Request',)
def request_class_factory(request):
"""
Builds and returns a request class, to be used as a replacement of Django's built-in.
In fact :class:`request.Request` needs to be mixed-in with a subclass of `HttpRequest` for use,
and we cannot do that before knowing which subclass of `HttpRequest` is used. So this function
takes a request instance as only argument, and returns a properly mixed-in request class.
"""
request_class = type(request)
return type(request_class.__name__, (Request, request_class), {})
class Request(object): class Request(object):
""" """
A mixin class allowing to enhance Django's standard HttpRequest. A wrapper allowing to enhance Django's standard HttpRequest.
""" """
_USE_FORM_OVERLOADING = True _USE_FORM_OVERLOADING = True
@ -44,24 +32,14 @@ class Request(object):
_CONTENTTYPE_PARAM = '_content_type' _CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content' _CONTENT_PARAM = '_content'
parsers = () def __init__(self, request=None, parsers=None):
""" """
The set of parsers that the request can handle. `parsers` is a list/tuple of parser instances and represents the set of psrsers
that the response can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module.
""" """
self.request = request
def __init__(self, request): if parsers is not None:
# this allows to "copy" a request object into a new instance self.parsers = parsers
# of our custom request class.
# First, we prepare the attributes to copy.
attrs_dict = request.__dict__.copy()
attrs_dict.pop('method', None)
attrs_dict['_raw_method'] = request.method
# Then, put them in the instance's own __dict__
self.__dict__ = attrs_dict
@property @property
def method(self): def method(self):
@ -111,22 +89,6 @@ class Request(object):
self._load_data_and_files() self._load_data_and_files()
return self._files return self._files
def _load_post_and_files(self):
"""
Overrides the parent's `_load_post_and_files` to isolate it
from the form overloading mechanism (see: `_perform_form_overloading`).
"""
# When self.POST or self.FILES are called they need to know the original
# HTTP method, not our overloaded HTTP method. So, we save our overloaded
# HTTP method and restore it after the call to parent.
method_mem = getattr(self, '_method', None)
self._method = self._raw_method
super(Request, self)._load_post_and_files()
if method_mem is None:
del self._method
else:
self._method = method_mem
def _load_data_and_files(self): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into self.DATA and self.FILES.
@ -145,7 +107,7 @@ class Request(object):
self._perform_form_overloading() self._perform_form_overloading()
# if the HTTP method was not overloaded, we take the raw HTTP method # if the HTTP method was not overloaded, we take the raw HTTP method
if not hasattr(self, '_method'): if not hasattr(self, '_method'):
self._method = self._raw_method self._method = self.request.method
def _get_stream(self): def _get_stream(self):
""" """
@ -172,7 +134,8 @@ class Request(object):
""" """
# We only need to use form overloading on form POST requests. # We only need to use form overloading on form POST requests.
if not self._USE_FORM_OVERLOADING or self._raw_method != 'POST' or not is_form_media_type(self._content_type): if (not self._USE_FORM_OVERLOADING or self.request.method != 'POST'
or not is_form_media_type(self._content_type)):
return return
# At this point we're committed to parsing the request as form data. # At this point we're committed to parsing the request as form data.
@ -199,10 +162,7 @@ class Request(object):
if stream is None or content_type is None: if stream is None or content_type is None:
return (None, None) return (None, None)
parsers = as_tuple(self.parsers) for parser in as_tuple(self.parsers):
for parser_cls in parsers:
parser = parser_cls(self)
if parser.can_handle_request(content_type): if parser.can_handle_request(content_type):
return parser.parse(stream) return parser.parse(stream)
@ -223,3 +183,26 @@ class Request(object):
Return the view's default parser class. Return the view's default parser class.
""" """
return self.parsers[0] return self.parsers[0]
def _get_parsers(self):
"""
This just provides a default when parsers havent' been set.
"""
if hasattr(self, '_parsers'):
return self._parsers
return ()
def _set_parsers(self, value):
self._parsers = value
parsers = property(_get_parsers, _set_parsers)
def __getattr__(self, name):
"""
When an attribute is not present on the calling instance, try to get it
from the original request.
"""
if hasattr(self.request, name):
return getattr(self.request, name)
else:
return super(Request, self).__getattribute__(name)

View File

@ -10,36 +10,19 @@ from djangorestframework.compat import RequestFactory
from djangorestframework.mixins import RequestMixin from djangorestframework.mixins import RequestMixin
from djangorestframework.parsers import FormParser, MultiPartParser, \ from djangorestframework.parsers import FormParser, MultiPartParser, \
PlainTextParser, JSONParser PlainTextParser, JSONParser
from djangorestframework.request import Request
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.request import Request from djangorestframework.request import Request
from djangorestframework.views import View from djangorestframework.views import View
from djangorestframework.request import request_class_factory
class MockView(View):
authentication = (UserLoggedInAuthentication,)
def post(self, request):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
return Response(status=status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
)
request_class = request_class_factory(RequestFactory().get('/'))
class RequestTestCase(TestCase): class RequestTestCase(TestCase):
def tearDown(self):
request_class.parsers = ()
def build_request(self, method, *args, **kwargs): def build_request(self, method, *args, **kwargs):
factory = RequestFactory() factory = RequestFactory()
method = getattr(factory, method) method = getattr(factory, method)
original_request = method(*args, **kwargs) original_request = method(*args, **kwargs)
return request_class(original_request) return Request(original_request)
class TestMethodOverloading(RequestTestCase): class TestMethodOverloading(RequestTestCase):
@ -67,14 +50,22 @@ class TestMethodOverloading(RequestTestCase):
class TestContentParsing(RequestTestCase): class TestContentParsing(RequestTestCase):
def tearDown(self):
request_class.parsers = ()
def build_request(self, method, *args, **kwargs): def build_request(self, method, *args, **kwargs):
factory = RequestFactory() factory = RequestFactory()
parsers = kwargs.pop('parsers', None)
method = getattr(factory, method) method = getattr(factory, method)
original_request = method(*args, **kwargs) original_request = method(*args, **kwargs)
return request_class(original_request) rkwargs = {}
if parsers is not None:
rkwargs['parsers'] = parsers
request = Request(original_request, **rkwargs)
# TODO: Just a hack because the parsers need a view. This will be fixed in the future
class Obj(object): pass
obj = Obj()
obj.request = request
for p in request.parsers:
p.view = obj
return request
def test_standard_behaviour_determines_no_content_GET(self): def test_standard_behaviour_determines_no_content_GET(self):
"""Ensure request.DATA returns None for GET request with no content.""" """Ensure request.DATA returns None for GET request with no content."""
@ -89,31 +80,35 @@ class TestContentParsing(RequestTestCase):
def test_standard_behaviour_determines_form_content_POST(self): def test_standard_behaviour_determines_form_content_POST(self):
"""Ensure request.DATA returns content for POST request with form content.""" """Ensure request.DATA returns content for POST request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser) parsers = (FormParser(), MultiPartParser())
request = self.build_request('post', '/', data=form_data)
request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.DATA.items(), form_data.items()) self.assertEqual(request.DATA.items(), form_data.items())
def test_standard_behaviour_determines_non_form_content_POST(self): def test_standard_behaviour_determines_non_form_content_POST(self):
"""Ensure request.DATA returns content for POST request with non-form content.""" """Ensure request.DATA returns content for POST request with non-form content."""
content = 'qwerty' content = 'qwerty'
content_type = 'text/plain' content_type = 'text/plain'
request_class.parsers = (PlainTextParser,) parsers = (PlainTextParser(),)
request = self.build_request('post', '/', content, content_type=content_type)
request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers)
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
def test_standard_behaviour_determines_form_content_PUT(self): def test_standard_behaviour_determines_form_content_PUT(self):
"""Ensure request.DATA returns content for PUT request with form content.""" """Ensure request.DATA returns content for PUT request with form content."""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser) parsers = (FormParser(), MultiPartParser())
request = self.build_request('put', '/', data=form_data)
request = self.build_request('put', '/', data=form_data, parsers=parsers)
self.assertEqual(request.DATA.items(), form_data.items()) self.assertEqual(request.DATA.items(), form_data.items())
def test_standard_behaviour_determines_non_form_content_PUT(self): def test_standard_behaviour_determines_non_form_content_PUT(self):
"""Ensure request.DATA returns content for PUT request with non-form content.""" """Ensure request.DATA returns content for PUT request with non-form content."""
content = 'qwerty' content = 'qwerty'
content_type = 'text/plain' content_type = 'text/plain'
request_class.parsers = (PlainTextParser,) parsers = (PlainTextParser(),)
request = self.build_request('put', '/', content, content_type=content_type)
request = self.build_request('put', '/', content, content_type=content_type, parsers=parsers)
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
def test_overloaded_behaviour_allows_content_tunnelling(self): def test_overloaded_behaviour_allows_content_tunnelling(self):
@ -122,16 +117,17 @@ class TestContentParsing(RequestTestCase):
content_type = 'text/plain' content_type = 'text/plain'
form_data = {Request._CONTENT_PARAM: content, form_data = {Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type} Request._CONTENTTYPE_PARAM: content_type}
request_class.parsers = (PlainTextParser,) parsers = (PlainTextParser(),)
request = self.build_request('post', '/', form_data)
request = self.build_request('post', '/', form_data, parsers=parsers)
self.assertEqual(request.DATA, content) self.assertEqual(request.DATA, content)
def test_accessing_post_after_data_form(self): def test_accessing_post_after_data_form(self):
"""Ensures request.POST can be accessed after request.DATA in form request""" """Ensures request.POST can be accessed after request.DATA in form request"""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser) parsers = (FormParser(), MultiPartParser())
request = self.build_request('post', '/', data=form_data)
request = self.build_request('post', '/', data=form_data)
self.assertEqual(request.DATA.items(), form_data.items()) self.assertEqual(request.DATA.items(), form_data.items())
self.assertEqual(request.POST.items(), form_data.items()) self.assertEqual(request.POST.items(), form_data.items())
@ -142,11 +138,9 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,) request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers)
request = self.build_request('post', '/', content, content_type=content_type)
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
self.assertEqual(request.POST.items(), []) self.assertEqual(request.POST.items(), [])
@ -157,22 +151,19 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,)
form_data = {Request._CONTENT_PARAM: content, form_data = {Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type} Request._CONTENTTYPE_PARAM: content_type}
request = self.build_request('post', '/', data=form_data) request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
self.assertEqual(request.POST.items(), form_data.items()) self.assertEqual(request.POST.items(), form_data.items())
def test_accessing_data_after_post_form(self): def test_accessing_data_after_post_form(self):
"""Ensures request.DATA can be accessed after request.POST in form request""" """Ensures request.DATA can be accessed after request.POST in form request"""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser) parsers = (FormParser, MultiPartParser)
request = self.build_request('post', '/', data=form_data) request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.POST.items(), form_data.items()) self.assertEqual(request.POST.items(), form_data.items())
self.assertEqual(request.DATA.items(), form_data.items()) self.assertEqual(request.DATA.items(), form_data.items())
@ -184,11 +175,9 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,) request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers)
request = self.build_request('post', '/', content, content_type=content_type)
post_items = request.POST.items() post_items = request.POST.items()
self.assertEqual(len(post_items), 1) self.assertEqual(len(post_items), 1)
@ -203,17 +192,28 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,)
form_data = {Request._CONTENT_PARAM: content, form_data = {Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type} Request._CONTENTTYPE_PARAM: content_type}
request = self.build_request('post', '/', data=form_data) request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.POST.items(), form_data.items()) self.assertEqual(request.POST.items(), form_data.items())
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
class MockView(View):
authentication = (UserLoggedInAuthentication,)
def post(self, request):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
return Response(status=status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
)
class TestContentParsingWithAuthentication(TestCase): class TestContentParsingWithAuthentication(TestCase):
urls = 'djangorestframework.tests.request' urls = 'djangorestframework.tests.request'

View File

@ -83,12 +83,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
renderer_classes = renderers.DEFAULT_RENDERERS renderer_classes = renderers.DEFAULT_RENDERERS
""" """
List of renderers the resource can serialize the response with, ordered by preference. List of renderer classes the resource can serialize the response with, ordered by preference.
""" """
parsers = parsers.DEFAULT_PARSERS parser_classes = parsers.DEFAULT_PARSERS
""" """
List of parsers the resource can parse the request with. List of parser classes the resource can parse the request with.
""" """
authentication = (authentication.UserLoggedInAuthentication, authentication = (authentication.UserLoggedInAuthentication,
@ -210,7 +210,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
try: try:
# Get a custom request, built form the original request instance # Get a custom request, built form the original request instance
self.request = request = self.get_request() request = self.prepare_request(request)
# `initial` is the opportunity to temper with the request, # `initial` is the opportunity to temper with the request,
# even completely replace it. # even completely replace it.
@ -229,7 +229,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
response = handler(request, *args, **kwargs) response = handler(request, *args, **kwargs)
# Prepare response for the response cycle. # Prepare response for the response cycle.
self.prepare_response(response) response = self.prepare_response(response)
# Pre-serialize filtering (eg filter complex objects into natively serializable types) # Pre-serialize filtering (eg filter complex objects into natively serializable types)
# TODO: ugly hack to handle both HttpResponse and Response. # TODO: ugly hack to handle both HttpResponse and Response.
@ -251,7 +251,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
'name': self.get_name(), 'name': self.get_name(),
'description': self.get_description(), 'description': self.get_description(),
'renders': self._rendered_media_types, 'renders': self._rendered_media_types,
'parses': request._parsed_media_types, 'parses': self._parsed_media_types,
} }
form = self.get_bound_form() form = self.get_bound_form()
if form is not None: if form is not None: