diff options
| author | Tom Christie | 2012-10-17 14:42:16 -0700 | 
|---|---|---|
| committer | Tom Christie | 2012-10-17 14:42:16 -0700 | 
| commit | bbd3728da615f688da6a5276c6a898990275f32c (patch) | |
| tree | 1566a18e4b4ad03a094c2c630b57d1530a9245d7 | |
| parent | 6717d654d0bbfdfca4aaea84a5b4814c4e5f7567 (diff) | |
| parent | fb56f215ae50da0aebe99e05036ece259fd3e6f1 (diff) | |
| download | django-rest-framework-bbd3728da615f688da6a5276c6a898990275f32c.tar.bz2 | |
Merge pull request #303 from tomchristie/parser_refactor
Parser refactor
| -rw-r--r-- | docs/api-guide/parsers.md | 20 | ||||
| -rw-r--r-- | docs/api-guide/renderers.md | 5 | ||||
| -rw-r--r-- | docs/tutorial/1-serialization.md | 7 | ||||
| -rw-r--r-- | rest_framework/parsers.py | 50 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 13 | ||||
| -rw-r--r-- | rest_framework/request.py | 21 | ||||
| -rw-r--r-- | rest_framework/tests/request.py | 2 | ||||
| -rw-r--r-- | rest_framework/views.py | 17 | 
8 files changed, 68 insertions, 67 deletions
| diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 4f145ba3..18a5872c 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -91,19 +91,27 @@ You will typically want to use both `FormParser` and `MultiPartParser` together  # Custom parsers -To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse_stream(self, stream, parser_context)` method. +To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, parser_context)` method.  The method should return the data that will be used to populate the `request.DATA` property. -The arguments passed to `.parse_stream()` are: +The arguments passed to `.parse()` are:  ### stream  A stream-like object representing the body of the request. +### media_type + +Optional.  If provided, this is the media type of the incoming request. + +Depending on the request's `Content-Type:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters.  For example `"text/plain; charset=utf-8"`. +  ### parser_context -If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content.  By default it includes the keys `'upload_handlers'` and `'meta'`, which contain the values of the `request.upload_handlers` and `request.meta` properties. +Optional.  If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. + +By default this will include the following keys: `view`, `request`, `args`, `kwargs`.  ## Example @@ -116,7 +124,7 @@ The following is an example plaintext parser that will populate the `request.DAT      media_type = 'text/plain' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Simply return a string representing the body of the request.          """ @@ -124,7 +132,7 @@ The following is an example plaintext parser that will populate the `request.DAT  ## Uploading file content -If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse_stream()` method.  `DataAndFiles` should be instantiated with two arguments.  The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property. +If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse()` method.  `DataAndFiles` should be instantiated with two arguments.  The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property.  For example: @@ -133,7 +141,7 @@ For example:          A naive raw file upload parser.          """ -        def parse_stream(self, stream, parser_context): +        def parse(self, stream, media_type=None, parser_context=None):              content = stream.read()              name = 'example.dat'              content_type = 'application/octet-stream' diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index c8addb32..24cca181 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -162,11 +162,14 @@ The request data, as set by the `Response()` instantiation.  ### `media_type=None` -Optional. If provided, this is the accepted media type, as determined by the content negotiation stage.  Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters.  For example `"application/json; nested=true"`. +Optional. If provided, this is the accepted media type, as determined by the content negotiation stage. + +Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters.  For example `"application/json; nested=true"`.  ### `renderer_context=None`  Optional. If provided, this is a dictionary of contextual information provided by the view. +  By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`.  ## Example diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e21433ba..5b58f293 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -134,12 +134,15 @@ We've now got a few comment instances to play with.  Let's take a look at serial  At this point we've translated the model instance into python native datatypes.  To finalise the serialization process we render the data into `json`. -    stream = JSONRenderer().render(serializer.data) -    stream +    content = JSONRenderer().render(serializer.data) +    content      # '{"id": 1, "email": "leila@example.com", "content": "nothing to say", "created": "2012-08-22T16:20:09.822"}'  Deserialization is similar.  First we parse a stream into python native datatypes...  +    import StringIO + +    stream = StringIO.StringIO(content)      data = JSONParser().parse(stream)  ...then we restore those native datatypes into to a fully populated object instance. diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 048b71e1..4841676c 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -1,14 +1,8 @@  """ -Django supports parsing the content of an HTTP request, but only for form POST requests. -That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well -to general HTTP requests. +Parsers are used to parse the content of incoming HTTP requests. -We need a method to be able to: - -1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT) - -2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded -   and multipart/form-data.  (eg also handle multipart/json) +They give us a generic way of being able to handle various media types +on the request, such as form content or json encoded data.  """  from django.http import QueryDict @@ -21,7 +15,6 @@ from xml.etree import ElementTree as ET  from xml.parsers.expat import ExpatError  import datetime  import decimal -from io import BytesIO  class DataAndFiles(object): @@ -33,29 +26,18 @@ class DataAndFiles(object):  class BaseParser(object):      """      All parsers should extend `BaseParser`, specifying a `media_type` -    attribute, and overriding the `.parse_stream()` method. +    attribute, and overriding the `.parse()` method.      """      media_type = None -    def parse(self, string_or_stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """ -        The main entry point to parsers.  This is a light wrapper around -        `parse_stream`, that instead handles both string and stream objects. -        """ -        if isinstance(string_or_stream, basestring): -            stream = BytesIO(string_or_stream) -        else: -            stream = string_or_stream -        return self.parse_stream(stream, parser_context) - -    def parse_stream(self, stream, parser_context=None): -        """ -        Given a stream to read from, return the deserialized output. -        Should return parsed data, or a DataAndFiles object consisting of the +        Given a stream to read from, return the parsed representation. +        Should return parsed data, or a `DataAndFiles` object consisting of the          parsed data and files.          """ -        raise NotImplementedError(".parse_stream() must be overridden.") +        raise NotImplementedError(".parse() must be overridden.")  class JSONParser(BaseParser): @@ -65,7 +47,7 @@ class JSONParser(BaseParser):      media_type = 'application/json' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Returns a 2-tuple of `(data, files)`. @@ -85,7 +67,7 @@ class YAMLParser(BaseParser):      media_type = 'application/yaml' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Returns a 2-tuple of `(data, files)`. @@ -105,7 +87,7 @@ class FormParser(BaseParser):      media_type = 'application/x-www-form-urlencoded' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Returns a 2-tuple of `(data, files)`. @@ -123,7 +105,7 @@ class MultiPartParser(BaseParser):      media_type = 'multipart/form-data' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Returns a DataAndFiles object. @@ -131,8 +113,10 @@ class MultiPartParser(BaseParser):          `.files` will be a `QueryDict` containing all the form files.          """          parser_context = parser_context or {} -        meta = parser_context['meta'] -        upload_handlers = parser_context['upload_handlers'] +        request = parser_context['request'] +        meta = request.META +        upload_handlers = request.upload_handlers +          try:              parser = DjangoMultiPartParser(meta, stream, upload_handlers)              data, files = parser.parse() @@ -148,7 +132,7 @@ class XMLParser(BaseParser):      media_type = 'application/xml' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          try:              tree = ET.parse(stream)          except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 94d253c9..23fd961b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1,9 +1,10 @@  """ -Renderers are used to serialize a View's output into specific media types. +Renderers are used to serialize a response into specific media types. -Django REST framework also provides HTML and PlainText renderers that help self-document the API, -by serializing the output along with documentation regarding the View, output status and headers, -and providing forms and links depending on the allowed methods, renderers and parsers on the View. +They give us a generic way of being able to handle various media types +on the response, such as JSON encoded data or HTML output. + +REST framework also provides an HTML renderer the renders the browseable API.  """  import string  from django import forms @@ -23,8 +24,8 @@ from rest_framework import serializers, parsers  class BaseRenderer(object):      """ -    All renderers must extend this class, set the :attr:`media_type` attribute, -    and override the :meth:`render` method. +    All renderers should extend this class, setting the `media_type` +    and `format` attributes, and override the `.render()` method.      """      media_type = None diff --git a/rest_framework/request.py b/rest_framework/request.py index 6f9cf09a..b9d55de4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -88,17 +88,12 @@ class Request(object):          self._stream = Empty          if self.parser_context is None: -            self.parser_context = self._default_parser_context(request) +            self.parser_context = {} +        self.parser_context['request'] = self      def _default_negotiator(self):          return api_settings.DEFAULT_CONTENT_NEGOTIATION() -    def _default_parser_context(self, request): -        return { -            'upload_handlers': request.upload_handlers, -            'meta': request.META, -        } -      @property      def method(self):          """ @@ -265,15 +260,19 @@ class Request(object):          May raise an `UnsupportedMediaType`, or `ParseError` exception.          """ -        if self.stream is None or self.content_type is None: +        stream = self.stream +        media_type = self.content_type + +        if stream is None or media_type is None:              return (None, None) -        parser = self.negotiator.select_parser(self.parsers, self.content_type) +        parser = self.negotiator.select_parser(self.parsers, media_type)          if not parser: -            raise exceptions.UnsupportedMediaType(self.content_type) +            raise exceptions.UnsupportedMediaType(media_type) + +        parsed = parser.parse(stream, media_type, self.parser_context) -        parsed = parser.parse(self.stream, self.parser_context)          # Parser classes may return the raw data, or a          # DataAndFiles object.  Unpack the result as required.          try: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f90bebf4..ff48f3fa 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory()  class PlainTextParser(BaseParser):      media_type = 'text/plain' -    def parse_stream(self, stream, parser_context=None): +    def parse(self, stream, media_type=None, parser_context=None):          """          Returns a 2-tuple of `(data, files)`. diff --git a/rest_framework/views.py b/rest_framework/views.py index 62fc92f9..066c0bb9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -158,12 +158,15 @@ class APIView(View):      def get_parser_context(self, http_request):          """ -        Returns a dict that is passed through to Parser.parse_stream(), +        Returns a dict that is passed through to Parser.parse(),          as the `parser_context` keyword argument.          """ +        # Note: Additionally `request` will also be added to the context +        #       by the Request object.          return { -            'upload_handlers': http_request.upload_handlers, -            'meta': http_request.META, +            'view': self, +            'args': getattr(self, 'args', ()), +            'kwargs': getattr(self, 'kwargs', {})          }      def get_renderer_context(self): @@ -171,13 +174,13 @@ class APIView(View):          Returns a dict that is passed through to Renderer.render(),          as the `renderer_context` keyword argument.          """ -        # Note: Additionally 'response' will also be set on the context, +        # Note: Additionally 'response' will also be added to the context,          #       by the Response object.          return {              'view': self, -            'request': self.request, -            'args': self.args, -            'kwargs': self.kwargs +            'args': getattr(self, 'args', ()), +            'kwargs': getattr(self, 'kwargs', {}), +            'request': getattr(self, 'request', None)          }      # API policy instantiation methods | 
