Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Tweaks
  • Loading branch information
lovelydinosaur committed Oct 10, 2016
commit 2eb62d4b23294e06281de6a8c01fc2f19a3b61f8
25 changes: 17 additions & 8 deletions rest_framework/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,40 @@
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList


def _force_text_recursive(data, code=None):
def _get_error_details(data, default_code=None):
"""
Descend into a nested data structure, forcing any
lazy translation strings or strings into `ErrorMessage`.
"""
if isinstance(data, list):
ret = [
_force_text_recursive(item, code) for item in data
_get_error_details(item, default_code) for item in data
]
if isinstance(data, ReturnList):
return ReturnList(ret, serializer=data.serializer)
return ret
elif isinstance(data, dict):
ret = {
key: _force_text_recursive(value, code)
key: _get_error_details(value, default_code)
for key, value in data.items()
}
if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer)
return ret

text = force_text(data)
code = getattr(data, 'code', code or 'invalid')
return ErrorMessage(text, code)
code = getattr(data, 'code', default_code)
return ErrorDetail(text, code)


class ErrorMessage(six.text_type):
class ErrorDetail(six.text_type):
"""
A string-like object that can additionally
"""
code = None

def __new__(cls, string, code=None):
self = super(ErrorMessage, cls).__new__(cls, string)
self = super(ErrorDetail, cls).__new__(cls, string)
self.code = code
return self

Expand Down Expand Up @@ -85,7 +88,13 @@ def __init__(self, detail, code=None):
# The details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
self.detail = _force_text_recursive(detail, code=code)

if code is None:
default_code = 'invalid'
else:
default_code = code

self.detail = _get_error_details(detail, default_code)

def __str__(self):
return six.text_type(self.detail)
Expand Down
10 changes: 5 additions & 5 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from rest_framework.compat import (
get_remote_field, unicode_repr, unicode_to_repr, value_from_object
)
from rest_framework.exceptions import ErrorMessage, ValidationError
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html, humanize_datetime, representation

Expand Down Expand Up @@ -224,14 +224,14 @@ def __init__(self, value, display_text, disabled=False):
yield Option(value='n/a', display_text=cutoff_text, disabled=True)


def get_error_messages(exc_info):
def get_error_detail(exc_info):
"""
Given a Django ValidationError, return a list of ErrorMessage,
Given a Django ValidationError, return a list of ErrorDetail,
with the `code` populated.
"""
code = getattr(exc_info, 'code', None) or 'invalid'
return [
ErrorMessage(msg, code=code)
ErrorDetail(msg, code=code)
for msg in exc_info.messages
]

Expand Down Expand Up @@ -537,7 +537,7 @@ def run_validators(self, value):
raise
errors.extend(exc.detail)
except DjangoValidationError as exc:
errors.extend(get_error_messages(exc))
errors.extend(get_error_detail(exc))
if errors:
raise ValidationError(errors)

Expand Down
29 changes: 13 additions & 16 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,32 +291,29 @@ def __new__(cls, name, bases, attrs):
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)


def get_validation_error_detail(exc):
def as_serializer_error(exc):
assert isinstance(exc, (ValidationError, DjangoValidationError))

if isinstance(exc, DjangoValidationError):
# Normally you should raise `serializers.ValidationError`
# inside your codebase, but we handle Django's validation
# exception class as well for simpler compat.
# Eg. Calling Model.clean() explicitly inside Serializer.validate()
return {
api_settings.NON_FIELD_ERRORS_KEY: get_error_messages(exc)
}
elif isinstance(exc.detail, dict):
detail = get_error_detail(exc)
else:
detail = exc.detail

if isinstance(detail, dict):
# If errors may be a dict we use the standard {key: list of values}.
# Here we ensure that all the values are *lists* of errors.
return {
key: value if isinstance(value, (list, dict)) else [value]
for key, value in exc.detail.items()
for key, value in detail.items()
}
elif isinstance(exc.detail, list):
elif isinstance(detail, list):
# Errors raised as a list are non-field errors.
return {
api_settings.NON_FIELD_ERRORS_KEY: exc.detail
api_settings.NON_FIELD_ERRORS_KEY: detail
}
# Errors raised as a string are non-field errors.
return {
api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
api_settings.NON_FIELD_ERRORS_KEY: [detail]
}


Expand Down Expand Up @@ -410,7 +407,7 @@ def run_validation(self, data=empty):
value = self.validate(value)
assert value is not None, '.validate() should return the validated data'
except (ValidationError, DjangoValidationError) as exc:
raise ValidationError(detail=get_validation_error_detail(exc))
raise ValidationError(detail=as_serializer_error(exc))

return value

Expand Down Expand Up @@ -440,7 +437,7 @@ def to_internal_value(self, data):
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = get_validation_error_detail(exc)
errors[field.field_name] = get_error_detail(exc)
except SkipField:
pass
else:
Expand Down Expand Up @@ -564,7 +561,7 @@ def run_validation(self, data=empty):
value = self.validate(value)
assert value is not None, '.validate() should return the validated data'
except (ValidationError, DjangoValidationError) as exc:
raise ValidationError(detail=get_validation_error_detail(exc))
raise ValidationError(detail=as_serializer_error(exc))

return value

Expand Down
44 changes: 32 additions & 12 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,39 @@
from django.test import TestCase
from django.utils.translation import ugettext_lazy as _

from rest_framework.exceptions import ErrorMessage, _force_text_recursive
from rest_framework.exceptions import ErrorDetail, _get_error_details


class ExceptionTestCase(TestCase):

def test_force_text_recursive(self):

s = "sfdsfggiuytraetfdlklj"
self.assertEqual(_force_text_recursive(_(s)), s)
assert isinstance(_force_text_recursive(_(s)), ErrorMessage)

self.assertEqual(_force_text_recursive({'a': _(s)})['a'], s)
assert isinstance(_force_text_recursive({'a': _(s)})['a'], ErrorMessage)

self.assertEqual(_force_text_recursive([[_(s)]])[0][0], s)
assert isinstance(_force_text_recursive([[_(s)]])[0][0], ErrorMessage)
def test_get_error_details(self):

example = "string"
lazy_example = _(example)

self.assertEqual(
_get_error_details(lazy_example),
example
)
assert isinstance(
_get_error_details(lazy_example),
ErrorDetail
)

self.assertEqual(
_get_error_details({'nested': lazy_example})['nested'],
example
)
assert isinstance(
_get_error_details({'nested': lazy_example})['nested'],
ErrorDetail
)

self.assertEqual(
_get_error_details([[lazy_example]])[0][0],
example
)
assert isinstance(
_get_error_details([[lazy_example]])[0][0],
ErrorDetail
)
2 changes: 1 addition & 1 deletion tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_nested_validation_error_detail(self):
}
})

self.assertEqual(serializers.get_validation_error_detail(e), {
self.assertEqual(serializers.as_serializer_error(e), {
'nested': {
'field': ['error'],
}
Expand Down