diff --git a/README.md b/README.md index e388667..e0577c8 100644 --- a/README.md +++ b/README.md @@ -9,28 +9,24 @@ Django Rest Framework renderer using [ujson](https://github.com/esnme/ultrajson) `pip install drf_ujson` -You can then set the `UJSONRenderer` class as your default renderer in your `settings.py` +You can then set the `UJSONRenderer` and `UJSONParser` classes as your default in your `settings.py` ```python REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES': ( 'drf_ujson.renderers.UJSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', ), - ... -} -``` - -Also you can set the `UJSONParser` class as your default parser in your `settings.py` - -```python -REST_FRAMEWORK = { 'DEFAULT_PARSER_CLASSES': ( 'drf_ujson.parsers.UJSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser', ), ... } ``` + ## Benchmarks This is on average 2.3x faster than the default JSON Serializer. diff --git a/drf_ujson/parsers.py b/drf_ujson/parsers.py index 9e153ee..b212f83 100644 --- a/drf_ujson/parsers.py +++ b/drf_ujson/parsers.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals +import codecs + from django.conf import settings from rest_framework.compat import six from rest_framework.parsers import BaseParser, ParseError -from rest_framework.renderers import JSONRenderer +from rest_framework import renderers +from rest_framework.settings import api_settings import ujson -__author__ = 'y.gavenchuk aka murminathor' -__all__ = ['UJSONParser', ] - - class UJSONParser(BaseParser): """ - Parses JSON-serialized data by ujson parser. + Parses JSON-serialized data. """ - media_type = 'application/json' - renderer_class = JSONRenderer + renderer_class = renderers.JSONRenderer + strict = api_settings.STRICT_JSON def parse(self, stream, media_type=None, parser_context=None): """ @@ -28,7 +26,8 @@ def parse(self, stream, media_type=None, parser_context=None): encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) try: - data = stream.read().decode(encoding) - return ujson.loads(data) + decoded_stream = codecs.getreader(encoding)(stream) + parse_constant = ujson.strict_constant if self.strict else None + return ujson.load(decoded_stream, parse_constant=parse_constant) except ValueError as exc: raise ParseError('JSON parse error - %s' % six.text_type(exc)) diff --git a/drf_ujson/renderers.py b/drf_ujson/renderers.py index 87b7b66..fc9a905 100644 --- a/drf_ujson/renderers.py +++ b/drf_ujson/renderers.py @@ -2,28 +2,109 @@ from rest_framework.compat import six from rest_framework.renderers import BaseRenderer import ujson +from django.http.multipartparser import parse_header +from rest_framework.settings import api_settings + + +def zero_as_none(value): + return None if value == 0 else value + + +# class UJSONRenderer(BaseRenderer): +# """ +# Renderer which serializes to JSON. +# Applies JSON's backslash-u character escaping for non-ascii characters. +# Uses the blazing-fast ujson library for serialization. +# """ +# +# media_type = 'application/json' +# format = 'json' +# ensure_ascii = True +# charset = None +# +# def get_indent(self, accepted_media_type, renderer_context): +# if accepted_media_type: +# # If the media type looks like 'application/json; indent=4', +# # then pretty print the result. +# # Note that we coerce `indent=0` into `indent=None`. +# base_media_type, params = parse_header(accepted_media_type.encode('ascii')) +# try: +# return zero_as_none(max(min(int(params['indent']), 8), 0)) +# except (KeyError, ValueError, TypeError): +# pass +# +# # If 'indent' is provided in the context, then pretty print the result. +# # E.g. If we're being called by the BrowsableAPIRenderer. +# return renderer_context.get('indent', None) +# +# def render(self, data, *args, **kwargs): +# +# if data is None: +# return bytes() +# +# ret = ujson.dumps(data, ensure_ascii=self.ensure_ascii) +# +# # force return value to unicode +# if isinstance(ret, six.text_type): +# return bytes(ret.encode('utf-8')) +# return ret class UJSONRenderer(BaseRenderer): """ Renderer which serializes to JSON. - Applies JSON's backslash-u character escaping for non-ascii characters. - Uses the blazing-fast ujson library for serialization. """ - media_type = 'application/json' format = 'json' - ensure_ascii = True + ensure_ascii = not api_settings.UNICODE_JSON + compact = api_settings.COMPACT_JSON + strict = api_settings.STRICT_JSON + + # We don't set a charset because JSON is a binary encoding, + # that can be encoded as utf-8, utf-16 or utf-32. + # See: https://www.ietf.org/rfc/rfc4627.txt + # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/ charset = None - def render(self, data, *args, **kwargs): + def get_indent(self, accepted_media_type, renderer_context): + if accepted_media_type: + # If the media type looks like 'application/json; indent=4', + # then pretty print the result. + # Note that we coerce `indent=0` into `indent=None`. + base_media_type, params = parse_header(accepted_media_type.encode('ascii')) + try: + return zero_as_none(max(min(int(params['indent']), 8), 0)) + except (KeyError, ValueError, TypeError): + pass + # If 'indent' is provided in the context, then pretty print the result. + # E.g. If we're being called by the BrowsableAPIRenderer. + return renderer_context.get('indent', None) + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render `data` into JSON, returning a bytestring. + """ if data is None: return bytes() - ret = ujson.dumps(data, ensure_ascii=self.ensure_ascii) + renderer_context = renderer_context or {} + indent = self.get_indent(accepted_media_type, renderer_context) + + ret = ujson.dumps( + data, + indent=indent, ensure_ascii=self.ensure_ascii, + ) - # force return value to unicode + # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, + # but if ensure_ascii=False, the return type is underspecified, + # and may (or may not) be unicode. + # On python 3.x json.dumps() returns unicode strings. if isinstance(ret, six.text_type): + # We always fully escape \u2028 and \u2029 to ensure we output JSON + # that is a strict javascript subset. If bytes were returned + # by json.dumps() then we don't have these characters in any case. + # See: http://timelessrepo.com/json-isnt-a-javascript-subset + ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029') return bytes(ret.encode('utf-8')) return ret