Repository: encode/django-rest-framework

Issue:
Settings should be updated when overridden in Django tests
DRF has its own settings object at `rest_framework.settings.api_settings`. It appears that these are generated once and not updated when `override_settings` is used in Django testing.
## Failing test

With this in a settings file:

```
REST_FRAMEWORK = {
    'PAGINATE_BY': 10,
}
```

This will fail:

```
from django.test.utils import override_settings
from rest_framework.settings import api_settings
from rest_framework.test import APITestCase


class TestSettings(TestCase):

    def test_settings_normal(self):
        self.assertEqual(settings.REST_FRAMEWORK['PAGINATE_BY'], 10)
        self.assertEqual(api_settings.PAGINATE_BY, 10)

    def test_settings_overridden(self):
        with override_settings(REST_FRAMEWORK={'PAGINATE_BY': 999}):
            self.assertEqual(settings.REST_FRAMEWORK['PAGINATE_BY'], 999)
            self.assertEqual(api_settings.PAGINATE_BY, 999)
```

`test_settings_overridden` FAILS because `PAGINATE_BY` in `api_settings` is still 10 during override, when it should have been overridden to 999.

This is unexpected behaviour - Django developers familiar with the Django settings and the settings object will expect to be able to override at testing time as per the [Django docs](https://docs.djangoproject.com/en/1.7/topics/settings/).
## Simple solutions?

There are probably some elegant solutions to wire in the build of `api_settings` to listen for updates made by the settings override functions in Django tests... however, what about using what's in DRF already?

There is a `temporary_setting` function in `tests.utils`: https://github.com/tomchristie/django-rest-framework/blob/master/tests/utils.py#L9 , which looks like it can do the job. If this could be included in the DRF package and documented then it could be a "good enough" solution maybe.



Discussion:
> which looks like it can do the job

Looking at it I'd say we have that in the tests because it isn't really adequate to go into the core package.
I guess I'd rather see us do something that works with `override_settings`.

I guess we probably want to look at hooking into the `setting_changed` signal. https://docs.djangoproject.com/en/1.4/ref/signals/#django.test.signals.setting_changed


Resolving patch:
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -18,6 +18,7 @@
 back to the defaults.
 """
 from __future__ import unicode_literals
+from django.test.signals import setting_changed
 from django.conf import settings
 from django.utils import importlib, six
 from rest_framework import ISO_8601
@@ -198,3 +199,13 @@ def __getattr__(self, attr):
 
 
 api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
+
+
+def reload_api_settings(*args, **kwargs):
+    global api_settings
+    setting, value = kwargs['setting'], kwargs['value']
+    if setting == 'REST_FRAMEWORK':
+        api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)
+
+
+setting_changed.connect(reload_api_settings)


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -5,13 +5,15 @@
 from django.conf.urls import patterns, url
 from django.core.urlresolvers import reverse
 from django.test import TestCase
+from django.test.utils import override_settings
 from django.utils import unittest
 from django.utils.dateparse import parse_date
+from django.utils.six.moves import reload_module
 from rest_framework import generics, serializers, status, filters
 from rest_framework.compat import django_filters
 from rest_framework.test import APIRequestFactory
 from .models import BaseFilterableItem, FilterableItem, BasicModel
-from .utils import temporary_setting
+
 
 factory = APIRequestFactory()
 
@@ -404,7 +406,9 @@ class SearchListView(generics.ListAPIView):
         )
 
     def test_search_with_nonstandard_search_param(self):
-        with temporary_setting('SEARCH_PARAM', 'query', module=filters):
+        with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}):
+            reload_module(filters)
+
             class SearchListView(generics.ListAPIView):
                 queryset = SearchFilterModel.objects.all()
                 serializer_class = SearchFilterSerializer
@@ -422,6 +426,8 @@ class SearchListView(generics.ListAPIView):
                 ]
             )
 
+        reload_module(filters)
+
 
 class OrderingFilterModel(models.Model):
     title = models.CharField(max_length=20)
@@ -642,7 +648,9 @@ class OrderingListView(generics.ListAPIView):
         )
 
     def test_ordering_with_nonstandard_ordering_param(self):
-        with temporary_setting('ORDERING_PARAM', 'order', filters):
+        with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}):
+            reload_module(filters)
+
             class OrderingListView(generics.ListAPIView):
                 queryset = OrderingFilterModel.objects.all()
                 serializer_class = OrderingFilterSerializer
@@ -662,6 +670,8 @@ class OrderingListView(generics.ListAPIView):
                 ]
             )
 
+        reload_module(filters)
+
 
 class SensitiveOrderingFilterModel(models.Model):
     username = models.CharField(max_length=20)
diff --git a/tests/utils.py b/tests/utils.py
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,30 +1,5 @@
-from contextlib import contextmanager
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import NoReverseMatch
-from django.utils import six
-from rest_framework.settings import api_settings
-
-
-@contextmanager
-def temporary_setting(setting, value, module=None):
-    """
-    Temporarily change value of setting for test.
-
-    Optionally reload given module, useful when module uses value of setting on
-    import.
-    """
-    original_value = getattr(api_settings, setting)
-    setattr(api_settings, setting, value)
-
-    if module is not None:
-        six.moves.reload_module(module)
-
-    yield
-
-    setattr(api_settings, setting, original_value)
-
-    if module is not None:
-        six.moves.reload_module(module)
 
 
 class MockObject(object):