diff --git a/rest_framework/settings.py b/rest_framework/settings.py index fc6dfecda..d4e8a1648 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,6 +18,8 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals +from collections import Iterable +from django.core.exceptions import ImproperlyConfigured from django.conf import settings from django.utils import importlib, six from rest_framework import ISO_8601 @@ -187,6 +189,29 @@ class APISettings(object): except KeyError: # Fall back to defaults val = self.defaults[attr] + else: + # Verify that the user hasn't accidentally given a string instead + # of an iterable, or vice-versa + default = self.defaults[attr] + if issubclass(val.__class__, Iterable) \ + and (not issubclass(default.__class__, Iterable) + or isinstance(val, six.string_types)): + raise ImproperlyConfigured( + 'The "{settings_key}" setting must be a list or tuple, but ' + 'got type "{type_name}" with value "{value}".'.format( + settings_key=attr, type_name=val.__class__.__name__, + value=val + ) + ) + elif isinstance(default, six.string_types) and not \ + isinstance(val, six.string_types): + raise ImproperlyConfigured( + 'The "{settings_key}" setting must be a string, but ' + 'got type "{type_name}" with value "{value}".'.format( + settings_key=attr, type_name=val.__class__.__name__, + value=val + ) + ) # Coerce import strings into classes if val and attr in self.import_strings: diff --git a/tests/test_settings.py b/tests/test_settings.py index f2ff4ca14..335957df6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals +from django.core.exceptions import ImproperlyConfigured +import pytest from django.test import TestCase +from django.utils import six from rest_framework.settings import APISettings @@ -15,3 +18,46 @@ class TestSettings(TestCase): }) with self.assertRaises(ImportError): settings.DEFAULT_RENDERER_CLASSES + + def test_bad_iterable_setting_class_raises_warning(self): + """ + Make sure errors are raised when settings which should be iterable are + not. + """ + settings = APISettings({ + 'DEFAULT_RENDERER_CLASSES': 'rest_framework.renderers.JSONRenderer' + }) + + # Trigger the exception + with pytest.raises(ImproperlyConfigured) as exc_info: + settings.DEFAULT_RENDERER_CLASSES + + # Construct the expected error message + text_type_name = str(six.text_type.__name__) + expected_error = ( + 'The "DEFAULT_RENDERER_CLASSES" setting must be a list or ' + 'tuple, but got type "{text_type_name}" with value ' + '"rest_framework.renderers.JSONRenderer".'.format( + text_type_name=text_type_name + ) + ) + assert exc_info.value.args[0] == expected_error + + def test_bad_string_setting_class_raises_warning(self): + """ + Make sure errors are raised when settings which should be strings are + not. + """ + settings = APISettings({ + 'DEFAULT_METADATA_CLASS': [] + }) + + # Trigger the exception + with pytest.raises(ImproperlyConfigured) as exc_info: + settings.DEFAULT_METADATA_CLASS + + expected_error = ( + 'The "DEFAULT_METADATA_CLASS" setting must be a string, but got ' + 'type "list" with value "[]".' + ) + assert exc_info.value.args[0] == expected_error