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

View File

@ -146,7 +146,7 @@ class Request(object):
self._load_method_and_content_type() self._load_method_and_content_type()
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
(self._data, self._files) = self._parse() self._data, self._files = self._parse()
def _load_method_and_content_type(self): def _load_method_and_content_type(self):
""" """
@ -201,11 +201,11 @@ class Request(object):
self._CONTENTTYPE_PARAM in self._data): self._CONTENTTYPE_PARAM in self._data):
self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0] self._content_type = self._data.pop(self._CONTENTTYPE_PARAM)[0]
self._stream = StringIO(self._data.pop(self._CONTENT_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): 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. May raise an `UnsupportedMediaType`, or `ParseError` exception.
""" """
@ -214,8 +214,14 @@ class Request(object):
for parser in self.get_parsers(): for parser in self.get_parsers():
if parser.can_handle_request(self.content_type): if parser.can_handle_request(self.content_type):
return parser.parse(self.stream, meta=self.META, parsed = parser.parse(self.stream, meta=self.META,
upload_handlers=self.upload_handlers) 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) raise UnsupportedMediaType(self._content_type)

View File

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

View File

@ -301,7 +301,7 @@ if YAMLRenderer:
parser = YAMLParser() parser = YAMLParser()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
(data, files) = parser.parse(StringIO(content)) data = parser.parse(StringIO(content))
self.assertEquals(obj, data) self.assertEquals(obj, data)
@ -392,7 +392,7 @@ class XMLRendererTestCase(TestCase):
content = StringIO(renderer.render(self._complex_data, 'application/xml')) content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser() 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)) 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) 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.conf.urls.defaults import patterns
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils import simplejson as json
from djangorestframework import status from djangorestframework import status
from djangorestframework.authentication import UserLoggedInAuthentication from djangorestframework.authentication import UserLoggedInAuthentication
@ -13,7 +12,6 @@ from djangorestframework.parsers import (
FormParser, FormParser,
MultiPartParser, MultiPartParser,
PlainTextParser, PlainTextParser,
JSONParser
) )
from djangorestframework.request import Request from djangorestframework.request import Request
from djangorestframework.response import Response from djangorestframework.response import Response

View File

@ -1,3 +1,6 @@
"""
Helper classes for parsers.
"""
import datetime import datetime
import decimal import decimal
from django.utils import timezone from django.utils import timezone
@ -6,10 +9,12 @@ from django.utils import simplejson as json
class JSONEncoder(json.JSONEncoder): 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): 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): if isinstance(o, datetime.datetime):
r = o.isoformat() r = o.isoformat()
if o.microsecond: 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 = CommentSerializer(instance=c1)
serializer.data 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`. At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.