From 4b691c402707775c3048a90531024f3bc5be6f91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 13:06:27 +0100 Subject: Change package name: djangorestframework -> rest_framework --- rest_framework/parsers.py | 260 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 rest_framework/parsers.py (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py new file mode 100644 index 00000000..b3423131 --- /dev/null +++ b/rest_framework/parsers.py @@ -0,0 +1,260 @@ +""" +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. + +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) +""" + +from django.http import QueryDict +from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser +from django.http.multipartparser import MultiPartParserError +from django.utils import simplejson as json +from rest_framework.compat import yaml +from rest_framework.exceptions import ParseError +from rest_framework.utils.mediatypes import media_type_matches +from xml.etree import ElementTree as ET +from rest_framework.compat import ETParseError +from xml.parsers.expat import ExpatError +import datetime +import decimal +from io import BytesIO + + +__all__ = ( + 'BaseParser', + 'JSONParser', + 'PlainTextParser', + 'FormParser', + 'MultiPartParser', + 'YAMLParser', + 'XMLParser' +) + + +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, + and overriding the :meth:`parse` method. + """ + + media_type = None + + def can_handle_request(self, content_type): + """ + Returns :const:`True` if this parser is able to deal with the given *content_type*. + + The default implementation for this function is to check the *content_type* + argument against the :attr:`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. + """ + return media_type_matches(self.media_type, content_type) + + def parse(self, string_or_stream, **opts): + """ + 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, **opts) + + def parse_stream(self, stream, **opts): + """ + Given a *stream* to read from, return the deserialized output. + Should return parsed data, or a DataAndFiles object consisting of the + parsed data and files. + """ + raise NotImplementedError(".parse_stream() must be overridden.") + + +class JSONParser(BaseParser): + """ + Parses JSON-serialized data. + """ + + media_type = 'application/json' + + def parse_stream(self, stream, **opts): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will be an object which is the parsed content of the response. + `files` will always be `None`. + """ + try: + return json.load(stream) + except ValueError, exc: + raise ParseError('JSON parse error - %s' % unicode(exc)) + + +class YAMLParser(BaseParser): + """ + Parses YAML-serialized data. + """ + + media_type = 'application/yaml' + + def parse_stream(self, stream, **opts): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will be an object which is the parsed content of the response. + `files` will always be `None`. + """ + try: + return yaml.safe_load(stream) + except (ValueError, yaml.parser.ParserError), exc: + raise ParseError('YAML parse error - %s' % unicode(exc)) + + +class PlainTextParser(BaseParser): + """ + Plain text parser. + """ + + media_type = 'text/plain' + + def parse_stream(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`. + """ + return stream.read() + + +class FormParser(BaseParser): + """ + Parser for form data. + """ + + media_type = 'application/x-www-form-urlencoded' + + def parse_stream(self, stream, **opts): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will be a :class:`QueryDict` containing all the form parameters. + `files` will always be :const:`None`. + """ + data = QueryDict(stream.read()) + return data + + +class MultiPartParser(BaseParser): + """ + Parser for multipart form data, which may include file data. + """ + + media_type = 'multipart/form-data' + + def parse_stream(self, stream, **opts): + """ + Returns a DataAndFiles object. + + `.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) + data, files = parser.parse() + return DataAndFiles(data, files) + except MultiPartParserError, exc: + raise ParseError('Multipart form parse error - %s' % unicode(exc)) + + +class XMLParser(BaseParser): + """ + XML parser. + """ + + media_type = 'application/xml' + + def parse_stream(self, stream, **opts): + 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 + + def _xml_convert(self, element): + """ + convert the xml `element` into the corresponding python object + """ + + children = element.getchildren() + + if len(children) == 0: + return self._type_convert(element.text) + else: + # if the fist child tag is list-item means all children are list-item + if children[0].tag == "list-item": + data = [] + for child in children: + data.append(self._xml_convert(child)) + else: + data = {} + for child in children: + data[child.tag] = self._xml_convert(child) + + return data + + def _type_convert(self, value): + """ + Converts the value returned by the XMl parse into the equivalent + Python type + """ + if value is None: + return value + + try: + return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + except ValueError: + pass + + try: + return int(value) + except ValueError: + pass + + try: + return decimal.Decimal(value) + except decimal.InvalidOperation: + pass + + return value + + +DEFAULT_PARSERS = ( + JSONParser, + FormParser, + MultiPartParser, + XMLParser +) + +if yaml: + DEFAULT_PARSERS += (YAMLParser, ) +else: + YAMLParser = None -- cgit v1.2.3 From 7efc6e820580fde5a38cbee5cec05cbc87ce36e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 13:22:07 +0100 Subject: Remove dumbass __all__ variables --- rest_framework/parsers.py | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index b3423131..a500060f 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -26,17 +26,6 @@ import decimal from io import BytesIO -__all__ = ( - 'BaseParser', - 'JSONParser', - 'PlainTextParser', - 'FormParser', - 'MultiPartParser', - 'YAMLParser', - 'XMLParser' -) - - class DataAndFiles(object): def __init__(self, data, files): self.data = data -- cgit v1.2.3 From 09a445b257532be69ffab69a3f62b84bfa90463d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Sep 2012 12:40:11 +0100 Subject: Bits of cleanup --- rest_framework/parsers.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index a500060f..5151b252 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -34,8 +34,8 @@ class DataAndFiles(object): class BaseParser(object): """ - All parsers should extend :class:`BaseParser`, specifying a :attr:`media_type` attribute, - and overriding the :meth:`parse` method. + All parsers should extend `BaseParser`, specifying a `media_type` + attribute, and overriding the `.parse_stream()` method. """ media_type = None @@ -66,7 +66,7 @@ class BaseParser(object): def parse_stream(self, stream, **opts): """ - Given a *stream* to read from, return the deserialized output. + Given a stream to read from, return the deserialized output. Should return parsed data, or a DataAndFiles object consisting of the parsed data and files. """ @@ -234,16 +234,3 @@ class XMLParser(BaseParser): pass return value - - -DEFAULT_PARSERS = ( - JSONParser, - FormParser, - MultiPartParser, - XMLParser -) - -if yaml: - DEFAULT_PARSERS += (YAMLParser, ) -else: - YAMLParser = None -- cgit v1.2.3 From 9d8bce8f5b0915223f57d9fe3d4b63029cfc64c2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 14:48:33 +0100 Subject: Remove Parser.can_handle_request() --- rest_framework/parsers.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 5151b252..5325a64b 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -15,11 +15,9 @@ from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError from django.utils import simplejson as json -from rest_framework.compat import yaml +from rest_framework.compat import yaml, ETParseError from rest_framework.exceptions import ParseError -from rest_framework.utils.mediatypes import media_type_matches from xml.etree import ElementTree as ET -from rest_framework.compat import ETParseError from xml.parsers.expat import ExpatError import datetime import decimal @@ -40,19 +38,6 @@ class BaseParser(object): media_type = None - def can_handle_request(self, content_type): - """ - Returns :const:`True` if this parser is able to deal with the given *content_type*. - - The default implementation for this function is to check the *content_type* - argument against the :attr:`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. - """ - return media_type_matches(self.media_type, content_type) - def parse(self, string_or_stream, **opts): """ The main entry point to parsers. This is a light wrapper around -- cgit v1.2.3 From 551c86c43a71f7dee5cce68c5142714301f6196f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 14 Oct 2012 22:43:07 +0100 Subject: Documentation for parsers --- rest_framework/parsers.py | 17 ----------------- 1 file changed, 17 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 5325a64b..672f6a16 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -98,23 +98,6 @@ class YAMLParser(BaseParser): raise ParseError('YAML parse error - %s' % unicode(exc)) -class PlainTextParser(BaseParser): - """ - Plain text parser. - """ - - media_type = 'text/plain' - - def parse_stream(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`. - """ - return stream.read() - - class FormParser(BaseParser): """ Parser for form data. -- cgit v1.2.3 From 9c1fba3483b7e81da0744464dcf23a5f12711de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 13:27:50 +0100 Subject: Tweak parsers to take parser_context --- rest_framework/parsers.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 672f6a16..048b71e1 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -38,7 +38,7 @@ class BaseParser(object): media_type = None - def parse(self, string_or_stream, **opts): + def parse(self, string_or_stream, 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. @@ -47,9 +47,9 @@ class BaseParser(object): stream = BytesIO(string_or_stream) else: stream = string_or_stream - return self.parse_stream(stream, **opts) + return self.parse_stream(stream, parser_context) - def parse_stream(self, stream, **opts): + 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 @@ -65,7 +65,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -85,7 +85,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -105,7 +105,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -123,15 +123,16 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a DataAndFiles object. `.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'] + parser_context = parser_context or {} + meta = parser_context['meta'] + upload_handlers = parser_context['upload_handlers'] try: parser = DjangoMultiPartParser(meta, stream, upload_handlers) data, files = parser.parse() @@ -147,7 +148,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: -- cgit v1.2.3 From 99d48f90030d174ef80498b48f56af6489865f0d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:07:56 +0100 Subject: Drop .parse_string_or_stream() - keep API minimal. --- rest_framework/parsers.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 048b71e1..6287b842 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -21,7 +21,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 +32,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): - """ - 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): + def parse(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 parsed data and files. """ - raise NotImplementedError(".parse_stream() must be overridden.") + raise NotImplementedError(".parse() must be overridden.") class JSONParser(BaseParser): @@ -65,7 +53,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -85,7 +73,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -105,7 +93,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -123,7 +111,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a DataAndFiles object. @@ -148,7 +136,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: -- cgit v1.2.3 From 4231995fbd80e45991975ab81d9e570a9f4b72d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:19:59 +0100 Subject: parser_context includes `view`, `request`, `args`, `kwargs`. (Not `meta` and `upload_handlers`) Consistency with renderer API. --- rest_framework/parsers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 6287b842..7e13c3d8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -119,8 +119,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() -- cgit v1.2.3 From fb56f215ae50da0aebe99e05036ece259fd3e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:39:07 +0100 Subject: Added `media_type` to `.parse()` - Consistency with renderer API. --- rest_framework/parsers.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'rest_framework/parsers.py') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 7e13c3d8..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 @@ -37,10 +31,10 @@ class BaseParser(object): media_type = None - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, 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() must be overridden.") @@ -53,7 +47,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -73,7 +67,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -93,7 +87,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -111,7 +105,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a DataAndFiles object. @@ -138,7 +132,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse(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: -- cgit v1.2.3