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.resources import Resource, FormResource, ModelResource
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
@ -32,7 +32,6 @@ __all__ = (
'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 ##########
@ -41,39 +40,43 @@ class RequestMixin(object):
`Mixin` class to enhance API of Django's standard `request`.
"""
_USE_FORM_OVERLOADING = True
_METHOD_PARAM = '_method'
_CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content'
parsers = ()
parser_classes = ()
"""
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.
"""
def get_request_class(self):
"""
Returns a subclass of Django's `HttpRequest` with a richer API,
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
request_class = Request
"""
The class to use as a wrapper for the original request object.
"""
def get_request(self):
def get_parsers(self):
"""
Returns a custom request instance, with data and attributes copied from the
original request.
Instantiates and returns the list of parsers that will be used by the request
to parse its content.
"""
request_class = self.get_request_class()
return request_class(self.request)
if not hasattr(self, '_parsers'):
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 ##########
@ -105,8 +108,8 @@ class ResponseMixin(object):
def prepare_response(self, response):
"""
Prepares the response for the response cycle. This has no effect if the
response is not an instance of :class:`response.Response`.
Prepares the response for the response cycle, and returns the prepared 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:
response.request = self.request
@ -124,6 +127,17 @@ class ResponseMixin(object):
self.response = 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
def _rendered_media_types(self):
"""
@ -138,24 +152,6 @@ class ResponseMixin(object):
"""
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 ##########

View File

@ -43,7 +43,7 @@ class BaseParser(object):
media_type = None
def __init__(self, view):
def __init__(self, view=None):
"""
Initialize the parser with the ``View`` instance as state,
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.
`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._get_upload_handlers()
upload_handlers = self.view.request._get_upload_handlers()
try:
django_parser = DjangoMultiPartParser(self.view.META, stream, upload_handlers)
django_parser = DjangoMultiPartParser(self.view.request.META, stream, upload_handlers)
except MultiPartParserError, exc:
raise ImmediateResponse(
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
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
- form overloading of HTTP method, content type and content
"""
@ -22,21 +22,9 @@ from StringIO import StringIO
__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):
"""
A mixin class allowing to enhance Django's standard HttpRequest.
A wrapper allowing to enhance Django's standard HttpRequest.
"""
_USE_FORM_OVERLOADING = True
@ -44,24 +32,14 @@ class Request(object):
_CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content'
parsers = ()
"""
The set of parsers that the request can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module.
"""
def __init__(self, request):
# this allows to "copy" a request object into a new instance
# 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
def __init__(self, request=None, parsers=None):
"""
`parsers` is a list/tuple of parser instances and represents the set of psrsers
that the response can handle.
"""
self.request = request
if parsers is not None:
self.parsers = parsers
@property
def method(self):
@ -111,22 +89,6 @@ class Request(object):
self._load_data_and_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):
"""
Parses the request content into self.DATA and self.FILES.
@ -145,7 +107,7 @@ class Request(object):
self._perform_form_overloading()
# if the HTTP method was not overloaded, we take the raw HTTP method
if not hasattr(self, '_method'):
self._method = self._raw_method
self._method = self.request.method
def _get_stream(self):
"""
@ -172,7 +134,8 @@ class Request(object):
"""
# 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
# 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:
return (None, None)
parsers = as_tuple(self.parsers)
for parser_cls in parsers:
parser = parser_cls(self)
for parser in as_tuple(self.parsers):
if parser.can_handle_request(content_type):
return parser.parse(stream)
@ -223,3 +183,26 @@ class Request(object):
Return the view's default parser class.
"""
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.parsers import FormParser, MultiPartParser, \
PlainTextParser, JSONParser
from djangorestframework.request import Request
from djangorestframework.response import Response
from djangorestframework.request import Request
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):
def tearDown(self):
request_class.parsers = ()
def build_request(self, method, *args, **kwargs):
factory = RequestFactory()
method = getattr(factory, method)
original_request = method(*args, **kwargs)
return request_class(original_request)
return Request(original_request)
class TestMethodOverloading(RequestTestCase):
@ -67,14 +50,22 @@ class TestMethodOverloading(RequestTestCase):
class TestContentParsing(RequestTestCase):
def tearDown(self):
request_class.parsers = ()
def build_request(self, method, *args, **kwargs):
factory = RequestFactory()
parsers = kwargs.pop('parsers', None)
method = getattr(factory, method)
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):
"""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):
"""Ensure request.DATA returns content for POST request with form content."""
form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser)
request = self.build_request('post', '/', data=form_data)
parsers = (FormParser(), MultiPartParser())
request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.DATA.items(), form_data.items())
def test_standard_behaviour_determines_non_form_content_POST(self):
"""Ensure request.DATA returns content for POST request with non-form content."""
content = 'qwerty'
content_type = 'text/plain'
request_class.parsers = (PlainTextParser,)
request = self.build_request('post', '/', content, content_type=content_type)
parsers = (PlainTextParser(),)
request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers)
self.assertEqual(request.DATA, content)
def test_standard_behaviour_determines_form_content_PUT(self):
"""Ensure request.DATA returns content for PUT request with form content."""
form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser)
request = self.build_request('put', '/', data=form_data)
parsers = (FormParser(), MultiPartParser())
request = self.build_request('put', '/', data=form_data, parsers=parsers)
self.assertEqual(request.DATA.items(), form_data.items())
def test_standard_behaviour_determines_non_form_content_PUT(self):
"""Ensure request.DATA returns content for PUT request with non-form content."""
content = 'qwerty'
content_type = 'text/plain'
request_class.parsers = (PlainTextParser,)
request = self.build_request('put', '/', content, content_type=content_type)
parsers = (PlainTextParser(),)
request = self.build_request('put', '/', content, content_type=content_type, parsers=parsers)
self.assertEqual(request.DATA, content)
def test_overloaded_behaviour_allows_content_tunnelling(self):
@ -122,16 +117,17 @@ class TestContentParsing(RequestTestCase):
content_type = 'text/plain'
form_data = {Request._CONTENT_PARAM: content,
Request._CONTENTTYPE_PARAM: content_type}
request_class.parsers = (PlainTextParser,)
request = self.build_request('post', '/', form_data)
parsers = (PlainTextParser(),)
request = self.build_request('post', '/', form_data, parsers=parsers)
self.assertEqual(request.DATA, content)
def test_accessing_post_after_data_form(self):
"""Ensures request.POST can be accessed after request.DATA in form request"""
form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser)
request = self.build_request('post', '/', data=form_data)
parsers = (FormParser(), MultiPartParser())
request = self.build_request('post', '/', data=form_data)
self.assertEqual(request.DATA.items(), form_data.items())
self.assertEqual(request.POST.items(), form_data.items())
@ -142,11 +138,9 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,)
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.items(), data.items())
self.assertEqual(request.POST.items(), [])
@ -157,22 +151,19 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
request_class.parsers = (JSONParser,)
parsers = (JSONParser(),)
form_data = {Request._CONTENT_PARAM: content,
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.POST.items(), form_data.items())
def test_accessing_data_after_post_form(self):
"""Ensures request.DATA can be accessed after request.POST in form request"""
form_data = {'qwerty': 'uiop'}
request_class.parsers = (FormParser, MultiPartParser)
request = self.build_request('post', '/', data=form_data)
parsers = (FormParser, MultiPartParser)
request = self.build_request('post', '/', data=form_data, parsers=parsers)
self.assertEqual(request.POST.items(), form_data.items())
self.assertEqual(request.DATA.items(), form_data.items())
@ -184,11 +175,9 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
parsers = (JSONParser(),)
request_class.parsers = (JSONParser,)
request = self.build_request('post', '/', content, content_type=content_type)
request = self.build_request('post', '/', content, content_type=content_type, parsers=parsers)
post_items = request.POST.items()
self.assertEqual(len(post_items), 1)
@ -203,17 +192,28 @@ class TestContentParsing(RequestTestCase):
data = {'qwerty': 'uiop'}
content = json.dumps(data)
content_type = 'application/json'
request_class.parsers = (JSONParser,)
parsers = (JSONParser(),)
form_data = {Request._CONTENT_PARAM: content,
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.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):
urls = 'djangorestframework.tests.request'

View File

@ -83,12 +83,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
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,
@ -210,7 +210,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
try:
# 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,
# even completely replace it.
@ -229,7 +229,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
response = handler(request, *args, **kwargs)
# 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)
# TODO: ugly hack to handle both HttpResponse and Response.
@ -251,7 +251,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
'name': self.get_name(),
'description': self.get_description(),
'renders': self._rendered_media_types,
'parses': request._parsed_media_types,
'parses': self._parsed_media_types,
}
form = self.get_bound_form()
if form is not None: