diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index bc20fcaa3..c46577937 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -15,6 +15,11 @@ from rest_framework import status from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList +class SafeReplacerDict(dict): + def __missing__(self, key): + return key + + def _get_error_details(data, default_code=None): """ Descend into a nested data structure, forcing any @@ -144,7 +149,7 @@ class ValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Invalid input.') default_code = 'invalid' - default_params = {} + default_params = SafeReplacerDict() def __init__(self, detail=None, code=None, params=None): if detail is None: @@ -157,6 +162,7 @@ class ValidationError(APIException): # For validation failures, we may collect many errors together, # so the details should always be coerced to a list if not already. if isinstance(detail, str): + #import pdb; pdb.set_trace() detail = [detail % params] elif isinstance(detail, ValidationError): detail = detail.detail @@ -166,6 +172,7 @@ class ValidationError(APIException): if isinstance(detail_item, ValidationError): final_detail += detail_item.detail else: + #import pdb; pdb.set_trace() final_detail += [detail_item % params if isinstance(detail_item, str) else detail_item] detail = final_detail elif not isinstance(detail, dict) and not isinstance(detail, list): diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 7b8b3190f..39d98d255 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -1,3 +1,4 @@ +import pytest from django.test import TestCase from rest_framework import serializers, status @@ -195,3 +196,20 @@ class TestValidationErrorWithDjangoStyle(TestCase): assert str(error.detail[1]) == 'Invalid value: 43' assert str(error.detail[2]) == 'Invalid value: 44' assert str(error.detail[3]) == 'Invalid value: 45' + + def test_validation_error_without_params(self): + """Ensure that substitutable errors can be emitted without params.""" + + # mimic the logic in fields.Field.run_validators by saving the exception + # detail into a list which will then be the detail for a new ValidationError. + # this should not throw a KeyError or a TypeError even though + # the string has a substitutable substring ... + errors = [] + try: + raise ValidationError('%(user)s') + except ValidationError as exc: + errors.extend(exc.detail) + + # ensure it raises the correct exception type as an input to a new ValidationError + with pytest.raises(ValidationError): + raise ValidationError(errors)