Parsers may return raw data, or a DataAndFiles object

This commit is contained in:
Tom Christie 2012-09-03 14:28:40 +01:00
parent d180e984e9
commit 7abef9ac3b
7 changed files with 40 additions and 30 deletions

View File

@ -36,6 +36,12 @@ __all__ = (
)
class DataAndFiles(object):
def __init__(self, data, files):
self.data = data
self.files = files
class BaseParser(object):
"""
All parsers should extend :class:`BaseParser`, specifying a :attr:`media_type` attribute,
@ -80,7 +86,7 @@ class JSONParser(BaseParser):
`files` will always be `None`.
"""
try:
return (json.load(stream), None)
return json.load(stream)
except ValueError, exc:
raise ParseError('JSON parse error - %s' % unicode(exc))
@ -100,7 +106,7 @@ class YAMLParser(BaseParser):
`files` will always be `None`.
"""
try:
return (yaml.safe_load(stream), None)
return yaml.safe_load(stream)
except (ValueError, yaml.parser.ParserError), exc:
raise ParseError('YAML parse error - %s' % unicode(exc))
@ -119,7 +125,7 @@ class PlainTextParser(BaseParser):
`data` will simply be a string representing the body of the request.
`files` will always be `None`.
"""
return (stream.read(), None)
return stream.read()
class FormParser(BaseParser):
@ -137,7 +143,7 @@ class FormParser(BaseParser):
`files` will always be :const:`None`.
"""
data = QueryDict(stream.read())
return (data, None)
return data
class MultiPartParser(BaseParser):
@ -149,16 +155,17 @@ class MultiPartParser(BaseParser):
def parse(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
Returns a DataAndFiles object.
`data` will be a :class:`QueryDict` containing all the form parameters.
`files` will be a :class:`QueryDict` containing all the form files.
`.data` will be a `QueryDict` containing all the form parameters.
`.files` will be a `QueryDict` containing all the form files.
"""
meta = opts['meta']
upload_handlers = opts['upload_handlers']
try:
parser = DjangoMultiPartParser(meta, stream, upload_handlers)
return parser.parse()
data, files = parser.parse()
return DataAndFiles(data, files)
except MultiPartParserError, exc:
raise ParseError('Multipart form parse error - %s' % unicode(exc))
@ -171,19 +178,13 @@ class XMLParser(BaseParser):
media_type = 'application/xml'
def parse(self, stream, **opts):
"""
Returns a 2-tuple of `(data, files)`.
`data` will simply be a string representing the body of the request.
`files` will always be `None`.
"""
try:
tree = ET.parse(stream)
except (ExpatError, ETParseError, ValueError), exc:
raise ParseError('XML parse error - %s' % unicode(exc))
data = self._xml_convert(tree.getroot())
return (data, None)
return data
def _xml_convert(self, element):
"""

View File

@ -146,7 +146,7 @@ class Request(object):
self._load_method_and_content_type()
if not _hasattr(self, '_data'):
(self._data, self._files) = self._parse()
self._data, self._files = self._parse()
def _load_method_and_content_type(self):
"""
@ -201,11 +201,11 @@ class Request(object):
self._CONTENTTYPE_PARAM in self._data):
self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0]
self._stream = StringIO(self._data.pop(self._CONTENT_PARAM)[0])
(self._data, self._files) = self._parse()
self._data, self._files = self._parse()
def _parse(self):
"""
Parse the request content.
Parse the request content, returning a two-tuple of (data, files)
May raise an `UnsupportedMediaType`, or `ParseError` exception.
"""
@ -214,8 +214,14 @@ class Request(object):
for parser in self.get_parsers():
if parser.can_handle_request(self.content_type):
return parser.parse(self.stream, meta=self.META,
upload_handlers=self.upload_handlers)
parsed = parser.parse(self.stream, meta=self.META,
upload_handlers=self.upload_handlers)
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
try:
return (parsed.data, parsed.files)
except AttributeError:
return (parsed, None)
raise UnsupportedMediaType(self._content_type)

View File

@ -153,7 +153,7 @@ class TestFormParser(TestCase):
parser = FormParser()
stream = StringIO(self.string)
(data, files) = parser.parse(stream)
data = parser.parse(stream)
self.assertEqual(Form(data).is_valid(), True)
@ -203,10 +203,10 @@ class TestXMLParser(TestCase):
def test_parse(self):
parser = XMLParser()
(data, files) = parser.parse(self._input)
data = parser.parse(self._input)
self.assertEqual(data, self._data)
def test_complex_data_parse(self):
parser = XMLParser()
(data, files) = parser.parse(self._complex_data_input)
data = parser.parse(self._complex_data_input)
self.assertEqual(data, self._complex_data)

View File

@ -301,7 +301,7 @@ if YAMLRenderer:
parser = YAMLParser()
content = renderer.render(obj, 'application/yaml')
(data, files) = parser.parse(StringIO(content))
data = parser.parse(StringIO(content))
self.assertEquals(obj, data)
@ -392,7 +392,7 @@ class XMLRendererTestCase(TestCase):
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser()
complex_data_out, dummy = parser.parse(content)
complex_data_out = parser.parse(content)
error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
self.assertEqual(self._complex_data, complex_data_out, error_msg)

View File

@ -4,7 +4,6 @@ Tests for content parsing, and form-overloaded content parsing.
from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import simplejson as json
from djangorestframework import status
from djangorestframework.authentication import UserLoggedInAuthentication
@ -13,7 +12,6 @@ from djangorestframework.parsers import (
FormParser,
MultiPartParser,
PlainTextParser,
JSONParser
)
from djangorestframework.request import Request
from djangorestframework.response import Response

View File

@ -1,3 +1,6 @@
"""
Helper classes for parsers.
"""
import datetime
import decimal
from django.utils import timezone
@ -6,10 +9,12 @@ from django.utils import simplejson as json
class JSONEncoder(json.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time and decimal types.
JSONEncoder subclass that knows how to encode date/time,
decimal types, and generators.
"""
def default(self, o):
# See "Date Time String Format" in the ECMA-262 specification.
# For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
if isinstance(o, datetime.datetime):
r = o.isoformat()
if o.microsecond:

View File

@ -127,7 +127,7 @@ We've now got a few comment instances to play with. Let's take a look at serial
serializer = CommentSerializer(instance=c1)
serializer.data
# {'email': u'leila@example.com', 'content': u'nothing to say', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
# {'email': u'leila@example.com', 'content': u'nothing to say', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774, tzinfo=<UTC>)}
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.