diff --git a/djangorestframework/contentnegotiation.py b/djangorestframework/contentnegotiation.py new file mode 100644 index 000000000..223919eff --- /dev/null +++ b/djangorestframework/contentnegotiation.py @@ -0,0 +1,63 @@ +from djangorestframework import exceptions +from djangorestframework.settings import api_settings +from djangorestframework.utils.mediatypes import order_by_precedence +import re + +MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') + + +class BaseContentNegotiation(object): + def determine_renderer(self, request, renderers): + raise NotImplementedError('.determine_renderer() must be implemented') + + +class DefaultContentNegotiation(object): + settings = api_settings + + def negotiate(self, request, renderers): + """ + Given a request and a list of renderers, return a two-tuple of: + (renderer, media type). + """ + accepts = self.get_accept_list(request) + + # Check the acceptable media types against each renderer, + # attempting more specific media types first + # NB. The inner loop here isn't as bad as it first looks :) + # Worst case is we're looping over len(accept_list) * len(self.renderers) + for media_type_set in order_by_precedence(accepts): + for renderer in renderers: + for media_type in media_type_set: + if renderer.can_handle_media_type(media_type): + return renderer, media_type + + raise exceptions.NotAcceptable(available_renderers=renderers) + + def get_accept_list(self, request): + """ + Given the incoming request, return a tokenised list of + media type strings. + """ + if self.settings.URL_ACCEPT_OVERRIDE: + # URL style accept override. eg. "?accept=application/json" + override = request.GET.get(self.settings.URL_ACCEPT_OVERRIDE) + if override: + return [override] + + if (self.settings.IGNORE_MSIE_ACCEPT_HEADER and + 'HTTP_USER_AGENT' in request.META and + MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and + request.META.get('HTTP_X_REQUESTED_WITH', '').lower() != 'xmlhttprequest'): + # Ignore MSIE's broken accept behavior except for AJAX requests + # and do something sensible instead + return ['text/html', '*/*'] + + if 'HTTP_ACCEPT' in request.META: + # Standard HTTP Accept negotiation + # Accept header specified + tokens = request.META['HTTP_ACCEPT'].split(',') + return [token.strip() for token in tokens] + + # Standard HTTP Accept negotiation + # No accept header specified + return ['*/*'] diff --git a/djangorestframework/exceptions.py b/djangorestframework/exceptions.py index 3f5b23f67..6930515df 100644 --- a/djangorestframework/exceptions.py +++ b/djangorestframework/exceptions.py @@ -39,6 +39,15 @@ class MethodNotAllowed(APIException): self.detail = (detail or self.default_detail) % method +class NotAcceptable(APIException): + status_code = status.HTTP_406_NOT_ACCEPTABLE + default_detail = "Could not satisfy the request's Accept header" + + def __init__(self, detail=None, available_renderers=None): + self.detail = detail or self.default_detail + self.available_renderers = available_renderers + + class UnsupportedMediaType(APIException): status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE default_detail = "Unsupported media type '%s' in request." diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 26e8cba14..729f81116 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -48,28 +48,22 @@ class BaseRenderer(object): def __init__(self, view=None): self.view = view - def can_handle_response(self, accept): - """ - Returns :const:`True` if this renderer is able to deal with the given - *accept* media type. + def can_handle_format(self, format): + return format == self.format - The default implementation for this function is to check the *accept* - argument against the :attr:`media_type` attribute set on the class to see if + def can_handle_media_type(self, media_type): + """ + Returns `True` if this renderer is able to deal with the given + media type. + + The default implementation for this function is to check the media type + argument against the media_type attribute set on the class to see if they match. - This may be overridden to provide for other behavior, but typically you'll - instead want to just set the :attr:`media_type` attribute on the class. + This may be overridden to provide for other behavior, but typically + you'll instead want to just set the `media_type` attribute on the class. """ - # TODO: format overriding must go out of here - format = None - if self.view is not None: - format = self.view.kwargs.get(self._FORMAT_QUERY_PARAM, None) - if format is None and self.view is not None: - format = self.view.request.GET.get(self._FORMAT_QUERY_PARAM, None) - - if format is not None: - return format == self.format - return media_type_matches(self.media_type, accept) + return media_type_matches(self.media_type, media_type) def render(self, obj=None, media_type=None): """ diff --git a/djangorestframework/response.py b/djangorestframework/response.py index e1366bdb7..205c0503c 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -1,97 +1,34 @@ -""" -The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes. - -`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned -from any view. It is a bit smarter than Django's `HttpResponse`, for it renders automatically -its content to a serial format by using a list of :mod:`renderers`. - -To determine the content type to which it must render, default behaviour is to use standard -HTTP Accept header content negotiation. But `Response` also supports overriding the content type -by specifying an ``_accept=`` parameter in the URL. Also, `Response` will ignore `Accept` headers -from Internet Explorer user agents and use a sensible browser `Accept` header instead. -""" - - -import re from django.template.response import SimpleTemplateResponse from django.core.handlers.wsgi import STATUS_CODE_TEXT -from djangorestframework.settings import api_settings -from djangorestframework.utils.mediatypes import order_by_precedence -from djangorestframework import status - - -MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )') - - -class NotAcceptable(Exception): - pass class Response(SimpleTemplateResponse): """ - An HttpResponse that may include content that hasn't yet been serialized. - - Kwargs: - - content(object). The raw content, not yet serialized. - This must be native Python data that renderers can handle. - (e.g.: `dict`, `str`, ...) - - renderer_classes(list/tuple). The renderers to use for rendering the response content. + An HttpResponse that allows it's data to be rendered into + arbitrary media types. """ - _ACCEPT_QUERY_PARAM = api_settings.URL_ACCEPT_OVERRIDE - _IGNORE_IE_ACCEPT_HEADER = True + def __init__(self, data=None, status=None, headers=None, + renderer=None, media_type=None): + """ + Alters the init arguments slightly. + For example, drop 'template_name', and instead use 'data'. - def __init__(self, content=None, status=None, headers=None, view=None, - request=None, renderer_classes=None, format=None): - # First argument taken by `SimpleTemplateResponse.__init__` is template_name, - # which we don't need + Setting 'renderer' and 'media_type' will typically be defered, + For example being set automatically by the `APIView`. + """ super(Response, self).__init__(None, status=status) - - self.raw_content = content - self.has_content_body = content is not None + self.data = data self.headers = headers and headers[:] or [] - self.view = view - self.request = request - self.renderer_classes = renderer_classes - self.format = format - - def get_renderers(self): - """ - Instantiates and returns the list of renderers the response will use. - """ - if self.renderer_classes is None: - renderer_classes = api_settings.DEFAULT_RENDERERS - else: - renderer_classes = self.renderer_classes - - if self.format: - return [cls(self.view) for cls in renderer_classes - if cls.format == self.format] - return [cls(self.view) for cls in renderer_classes] + self.renderer = renderer + self.media_type = media_type @property def rendered_content(self): - """ - The final rendered content. Accessing this attribute triggers the - complete rendering cycle: selecting suitable renderer, setting - response's actual content type, rendering data. - """ - renderer, media_type = self._determine_renderer() - - # Set the media type of the response - self['Content-Type'] = renderer.media_type - - # Render the response content - if self.has_content_body: - return renderer.render(self.raw_content, media_type) - return renderer.render() - - def render(self): - try: - return super(Response, self).render() - except NotAcceptable: - response = self._get_406_response() - return response.render() + self['Content-Type'] = self.media_type + if self.data is None: + return self.renderer.render() + return self.renderer.render(self.data, self.media_type) @property def status_text(self): @@ -100,74 +37,3 @@ class Response(SimpleTemplateResponse): Provided for convenience. """ return STATUS_CODE_TEXT.get(self.status_code, '') - - def _determine_accept_list(self): - """ - Returns a list of accepted media types. This list is determined from : - - 1. overload with `_ACCEPT_QUERY_PARAM` - 2. `Accept` header of the request - - If those are useless, a default value is returned instead. - """ - request = self.request - - if (self._ACCEPT_QUERY_PARAM and - request.GET.get(self._ACCEPT_QUERY_PARAM, None)): - # Use _accept parameter override - return [request.GET.get(self._ACCEPT_QUERY_PARAM)] - elif (self._IGNORE_IE_ACCEPT_HEADER and - 'HTTP_USER_AGENT' in request.META and - MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and - request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'): - # Ignore MSIE's broken accept behavior except for AJAX requests - # and do something sensible instead - return ['text/html', '*/*'] - elif 'HTTP_ACCEPT' in request.META: - # Use standard HTTP Accept negotiation - return [token.strip() for token in request.META['HTTP_ACCEPT'].split(',')] - else: - # No accept header specified - return ['*/*'] - - def _determine_renderer(self): - """ - Determines the appropriate renderer for the output, given the list of - accepted media types, and the :attr:`renderer_classes` set on this class. - - Returns a 2-tuple of `(renderer, media_type)` - - See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - """ - - renderers = self.get_renderers() - accepts = self._determine_accept_list() - - # Not acceptable response - Ignore accept header. - if self.status_code == 406: - return (renderers[0], renderers[0].media_type) - - # Check the acceptable media types against each renderer, - # attempting more specific media types first - # NB. The inner loop here isn't as bad as it first looks :) - # Worst case is we're looping over len(accept_list) * len(self.renderers) - for media_type_set in order_by_precedence(accepts): - for renderer in renderers: - for media_type in media_type_set: - if renderer.can_handle_response(media_type): - return renderer, media_type - - # No acceptable renderers were found - raise NotAcceptable - - def _get_406_response(self): - renderer = self.renderer_classes[0] - return Response( - { - 'detail': 'Could not satisfy the client\'s Accept header', - 'available_types': [renderer.media_type - for renderer in self.renderer_classes] - }, - status=status.HTTP_406_NOT_ACCEPTABLE, - view=self.view, request=self.request, renderer_classes=[renderer]) diff --git a/djangorestframework/settings.py b/djangorestframework/settings.py index e5181f4b4..952157129 100644 --- a/djangorestframework/settings.py +++ b/djangorestframework/settings.py @@ -38,6 +38,7 @@ DEFAULTS = { ), 'DEFAULT_PERMISSIONS': (), 'DEFAULT_THROTTLES': (), + 'DEFAULT_CONTENT_NEGOTIATION': 'djangorestframework.contentnegotiation.DefaultContentNegotiation', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, @@ -45,7 +46,8 @@ DEFAULTS = { 'FORM_METHOD_OVERRIDE': '_method', 'FORM_CONTENT_OVERRIDE': '_content', 'FORM_CONTENTTYPE_OVERRIDE': '_content_type', - 'URL_ACCEPT_OVERRIDE': '_accept', + 'URL_ACCEPT_OVERRIDE': 'accept', + 'IGNORE_MSIE_ACCEPT_HEADER': True, 'FORMAT_SUFFIX_KWARG': 'format' } @@ -58,8 +60,9 @@ IMPORT_STRINGS = ( 'DEFAULT_AUTHENTICATION', 'DEFAULT_PERMISSIONS', 'DEFAULT_THROTTLES', + 'DEFAULT_CONTENT_NEGOTIATION', 'UNAUTHENTICATED_USER', - 'UNAUTHENTICATED_TOKEN' + 'UNAUTHENTICATED_TOKEN', ) @@ -68,7 +71,7 @@ def perform_import(val, setting): If the given setting is a string import notation, then perform the necessary import or imports. """ - if val is None or setting not in IMPORT_STRINGS: + if val is None or not setting in IMPORT_STRINGS: return val if isinstance(val, basestring): @@ -88,10 +91,7 @@ def import_from_string(val, setting): module_path, class_name = '.'.join(parts[:-1]), parts[-1] module = importlib.import_module(module_path) return getattr(module, class_name) - except Exception, e: - import traceback - tb = traceback.format_exc() - import pdb; pdb.set_trace() + except: msg = "Could not import '%s' for API setting '%s'" % (val, setting) raise ImportError(msg) diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py index 51e3660c5..74eae4381 100644 --- a/djangorestframework/tests/request.py +++ b/djangorestframework/tests/request.py @@ -7,7 +7,7 @@ from django.test import TestCase, Client from djangorestframework import status from djangorestframework.authentication import SessionAuthentication -from djangorestframework.utils import RequestFactory +from djangorestframework.compat import RequestFactory from djangorestframework.parsers import ( FormParser, MultiPartParser, @@ -22,33 +22,21 @@ factory = RequestFactory() class TestMethodOverloading(TestCase): - def test_GET_method(self): + def test_method(self): """ - GET requests identified. + Request methods should be same as underlying request. """ - request = factory.get('/') + request = Request(factory.get('/')) self.assertEqual(request.method, 'GET') - - def test_POST_method(self): - """ - POST requests identified. - """ - request = factory.post('/') + request = Request(factory.post('/')) self.assertEqual(request.method, 'POST') - def test_HEAD_method(self): - """ - HEAD requests identified. - """ - request = factory.head('/') - self.assertEqual(request.method, 'HEAD') - def test_overloaded_method(self): """ POST requests can be overloaded to another method by setting a reserved form field """ - request = factory.post('/', {Request._METHOD_PARAM: 'DELETE'}) + request = Request(factory.post('/', {Request._METHOD_PARAM: 'DELETE'})) self.assertEqual(request.method, 'DELETE') @@ -57,14 +45,14 @@ class TestContentParsing(TestCase): """ Ensure request.DATA returns None for GET request with no content. """ - request = factory.get('/') + request = Request(factory.get('/')) self.assertEqual(request.DATA, None) def test_standard_behaviour_determines_no_content_HEAD(self): """ Ensure request.DATA returns None for HEAD request. """ - request = factory.head('/') + request = Request(factory.head('/')) self.assertEqual(request.DATA, None) def test_standard_behaviour_determines_form_content_POST(self): @@ -72,8 +60,8 @@ class TestContentParsing(TestCase): Ensure request.DATA returns content for POST request with form content. """ data = {'qwerty': 'uiop'} - parsers = (FormParser, MultiPartParser) - request = factory.post('/', data, parser=parsers) + request = Request(factory.post('/', data)) + request.parser_classes = (FormParser, MultiPartParser) self.assertEqual(request.DATA.items(), data.items()) def test_standard_behaviour_determines_non_form_content_POST(self): @@ -83,9 +71,8 @@ class TestContentParsing(TestCase): """ content = 'qwerty' content_type = 'text/plain' - parsers = (PlainTextParser,) - request = factory.post('/', content, content_type=content_type, - parsers=parsers) + request = Request(factory.post('/', content, content_type=content_type)) + request.parser_classes = (PlainTextParser,) self.assertEqual(request.DATA, content) def test_standard_behaviour_determines_form_content_PUT(self): @@ -93,17 +80,17 @@ class TestContentParsing(TestCase): Ensure request.DATA returns content for PUT request with form content. """ data = {'qwerty': 'uiop'} - parsers = (FormParser, MultiPartParser) from django import VERSION if VERSION >= (1, 5): from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart - request = factory.put('/', encode_multipart(BOUNDARY, data), parsers=parsers, - content_type=MULTIPART_CONTENT) + request = Request(factory.put('/', encode_multipart(BOUNDARY, data), + content_type=MULTIPART_CONTENT)) else: - request = factory.put('/', data, parsers=parsers) + request = Request(factory.put('/', data)) + request.parser_classes = (FormParser, MultiPartParser) self.assertEqual(request.DATA.items(), data.items()) def test_standard_behaviour_determines_non_form_content_PUT(self): @@ -113,9 +100,8 @@ class TestContentParsing(TestCase): """ content = 'qwerty' content_type = 'text/plain' - parsers = (PlainTextParser, ) - request = factory.put('/', content, content_type=content_type, - parsers=parsers) + request = Request(factory.put('/', content, content_type=content_type)) + request.parser_classes = (PlainTextParser, ) self.assertEqual(request.DATA, content) def test_overloaded_behaviour_allows_content_tunnelling(self): @@ -128,8 +114,8 @@ class TestContentParsing(TestCase): Request._CONTENT_PARAM: content, Request._CONTENTTYPE_PARAM: content_type } - parsers = (PlainTextParser, ) - request = factory.post('/', data, parsers=parsers) + request = Request(factory.post('/', data)) + request.parser_classes = (PlainTextParser, ) self.assertEqual(request.DATA, content) # def test_accessing_post_after_data_form(self): diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index 0483d826d..cdddfff4e 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -4,10 +4,10 @@ import unittest from django.conf.urls.defaults import patterns, url, include from django.test import TestCase -from djangorestframework.response import Response, NotAcceptable +from djangorestframework.response import Response from djangorestframework.views import APIView from djangorestframework.compat import RequestFactory -from djangorestframework import status +from djangorestframework import status, exceptions from djangorestframework.renderers import ( BaseRenderer, JSONRenderer, @@ -91,7 +91,7 @@ class TestResponseDetermineRenderer(TestCase): accept_list = ['application/json'] renderer_classes = (MockPickleRenderer, ) response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes) - self.assertRaises(NotAcceptable, response._determine_renderer) + self.assertRaises(exceptions.NotAcceptable, response._determine_renderer) class TestResponseRenderContent(TestCase): diff --git a/djangorestframework/utils/__init__.py b/djangorestframework/utils/__init__.py index bb5bb6d7e..f53ac0b80 100644 --- a/djangorestframework/utils/__init__.py +++ b/djangorestframework/utils/__init__.py @@ -1,9 +1,6 @@ from django.utils.encoding import smart_unicode from django.utils.xmlutils import SimplerXMLGenerator - from djangorestframework.compat import StringIO -from djangorestframework.compat import RequestFactory as DjangoRequestFactory -from djangorestframework.request import Request import re import xml.etree.ElementTree as ET @@ -102,38 +99,3 @@ class XMLRenderer(): def dict2xml(input): return XMLRenderer().dict2xml(input) - - -class RequestFactory(DjangoRequestFactory): - """ - Replicate RequestFactory, but return Request, not HttpRequest. - """ - def get(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).get(*args, **kwargs) - return Request(request, parsers) - - def post(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).post(*args, **kwargs) - return Request(request, parsers) - - def put(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).put(*args, **kwargs) - return Request(request, parsers) - - def delete(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).delete(*args, **kwargs) - return Request(request, parsers) - - def head(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).head(*args, **kwargs) - return Request(request, parsers) - - def options(self, *args, **kwargs): - parsers = kwargs.pop('parsers', None) - request = super(RequestFactory, self).options(*args, **kwargs) - return Request(request, parsers) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 9debee19c..2629663a1 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -54,11 +54,14 @@ def _camelcase_to_spaces(content): class APIView(_View): + settings = api_settings + renderer_classes = api_settings.DEFAULT_RENDERERS parser_classes = api_settings.DEFAULT_PARSERS authentication_classes = api_settings.DEFAULT_AUTHENTICATION throttle_classes = api_settings.DEFAULT_THROTTLES permission_classes = api_settings.DEFAULT_PERMISSIONS + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION @classmethod def as_view(cls, **initkwargs): @@ -169,6 +172,27 @@ class APIView(_View): """ return self.renderer_classes[0] + def get_format_suffix(self, **kwargs): + """ + Determine if the request includes a '.json' style format suffix + """ + if self.settings.FORMAT_SUFFIX_KWARG: + return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) + + def get_renderers(self, format=None): + """ + Instantiates and returns the list of renderers that this view can use. + """ + return [renderer(self) for renderer in self.renderer_classes] + + def filter_renderers(self, renderers, format=None): + """ + If format suffix such as '.json' is supplied, filter the + list of valid renderers for this request. + """ + return [renderer for renderer in renderers + if renderer.can_handle_format(format)] + def get_permissions(self): """ Instantiates and returns the list of permissions that this view requires. @@ -177,10 +201,28 @@ class APIView(_View): def get_throttles(self): """ - Instantiates and returns the list of thottles that this view requires. + Instantiates and returns the list of thottles that this view uses. """ return [throttle(self) for throttle in self.throttle_classes] + def content_negotiation(self, request): + """ + Determine which renderer and media type to use render the response. + """ + renderers = self.get_renderers() + + if self.format: + # If there is a '.json' style format suffix, only use + # renderers that accept that format. + fallback = renderers[0] + renderers = self.filter_renderers(renderers, self.format) + if not renderers: + self.format404 = True + return (fallback, fallback.media_type) + + conneg = self.content_negotiation_class() + return conneg.negotiate(request, renderers) + def check_permissions(self, request, obj=None): """ Check if request should be permitted. @@ -204,35 +246,43 @@ class APIView(_View): return Request(request, parser_classes=self.parser_classes, authentication_classes=self.authentication_classes) + def initial(self, request, *args, **kwargs): + """ + Runs anything that needs to occur prior to calling the method handlers. + """ + self.format = self.get_format_suffix(**kwargs) + self.renderer, self.media_type = self.content_negotiation(request) + self.check_permissions(request) + self.check_throttles(request) + # If the request included a non-existant .format URL suffix, + # raise 404, but only after first making permission checks. + if getattr(self, 'format404', None): + raise Http404() + def finalize_response(self, request, response, *args, **kwargs): """ Returns the final response object. """ if isinstance(response, Response): - response.view = self - response.request = request - response.renderer_classes = self.renderer_classes - if api_settings.FORMAT_SUFFIX_KWARG: - response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None) + response.renderer = self.renderer + response.media_type = self.media_type for key, value in self.headers.items(): response[key] = value return response - def initial(self, request, *args, **kwargs): - """ - Runs anything that needs to occur prior to calling the method handlers. - """ - self.check_permissions(request) - self.check_throttles(request) - def handle_exception(self, exc): """ Handle any exception that occurs, by returning an appropriate response, or re-raising the error. """ - if isinstance(exc, exceptions.Throttled): + if isinstance(exc, exceptions.NotAcceptable): + # Fall back to default renderer + self.renderer = exc.available_renderers[0] + self.media_type = exc.available_renderers[0].media_type + elif isinstance(exc, exceptions.Throttled): + # Throttle wait header self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait if isinstance(exc, exceptions.APIException): @@ -250,14 +300,8 @@ class APIView(_View): @csrf_exempt def dispatch(self, request, *args, **kwargs): """ - `APIView.dispatch()` is pretty much the same as Django's regular - `View.dispatch()`, except that it includes hooks to: - - * Initialize the request object. - * Finalize the response object. - * Handle exceptions that occur in the handler method. - * An initial hook for code such as permission checking that should - occur prior to running the method handlers. + `.dispatch()` is pretty much the same as Django's regular dispatch, + but with extra hooks for startup, finalize, and exception handling. """ request = self.initialize_request(request, *args, **kwargs) self.request = request @@ -270,7 +314,8 @@ class APIView(_View): # Get the appropriate handler method if request.method.lower() in self.http_method_names: - handler = getattr(self, request.method.lower(), self.http_method_not_allowed) + handler = getattr(self, request.method.lower(), + self.http_method_not_allowed) else: handler = self.http_method_not_allowed