Browse Source

Added modify_settings to alter settings containing lists of values.

Aymeric Augustin 11 years ago
parent
commit
5241763c81

+ 2 - 2
django/test/__init__.py

@@ -8,10 +8,10 @@ from django.test.testcases import (
     SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
     SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
     skipUnlessDBFeature
     skipUnlessDBFeature
 )
 )
-from django.test.utils import override_settings
+from django.test.utils import modify_settings, override_settings
 
 
 __all__ = [
 __all__ = [
     'Client', 'RequestFactory', 'TestCase', 'TransactionTestCase',
     'Client', 'RequestFactory', 'TestCase', 'TransactionTestCase',
     'SimpleTestCase', 'LiveServerTestCase', 'skipIfDBFeature',
     'SimpleTestCase', 'LiveServerTestCase', 'skipIfDBFeature',
-    'skipUnlessDBFeature', 'override_settings',
+    'skipUnlessDBFeature', 'modify_settings', 'override_settings',
 ]
 ]

+ 20 - 7
django/test/testcases.py

@@ -32,7 +32,7 @@ from django.test.client import Client
 from django.test.html import HTMLParseError, parse_html
 from django.test.html import HTMLParseError, parse_html
 from django.test.signals import setting_changed, template_rendered
 from django.test.signals import setting_changed, template_rendered
 from django.test.utils import (CaptureQueriesContext, ContextList,
 from django.test.utils import (CaptureQueriesContext, ContextList,
-    override_settings, compare_xml)
+    override_settings, modify_settings, compare_xml)
 from django.utils.encoding import force_text
 from django.utils.encoding import force_text
 from django.utils import six
 from django.utils import six
 from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit, urlparse, unquote
 from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit, urlparse, unquote
@@ -164,7 +164,8 @@ class SimpleTestCase(unittest.TestCase):
     # The class we'll use for the test client self.client.
     # The class we'll use for the test client self.client.
     # Can be overridden in derived classes.
     # Can be overridden in derived classes.
     client_class = Client
     client_class = Client
-    _custom_settings = None
+    _overridden_settings = None
+    _modified_settings = None
 
 
     def __call__(self, result=None):
     def __call__(self, result=None):
         """
         """
@@ -197,9 +198,12 @@ class SimpleTestCase(unittest.TestCase):
         * If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
         * If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
         * Clearing the mail test outbox.
         * Clearing the mail test outbox.
         """
         """
-        if self._custom_settings:
-            self._overridden = override_settings(**self._custom_settings)
-            self._overridden.enable()
+        if self._overridden_settings:
+            self._overridden_context = override_settings(**self._overridden_settings)
+            self._overridden_context.enable()
+        if self._modified_settings:
+            self._modified_context = modify_settings(self._modified_settings)
+            self._modified_context.enable()
         self.client = self.client_class()
         self.client = self.client_class()
         self._urlconf_setup()
         self._urlconf_setup()
         mail.outbox = []
         mail.outbox = []
@@ -217,8 +221,10 @@ class SimpleTestCase(unittest.TestCase):
         * Putting back the original ROOT_URLCONF if it was changed.
         * Putting back the original ROOT_URLCONF if it was changed.
         """
         """
         self._urlconf_teardown()
         self._urlconf_teardown()
-        if self._custom_settings:
-            self._overridden.disable()
+        if self._modified_settings:
+            self._modified_context.disable()
+        if self._overridden_settings:
+            self._overridden_context.disable()
 
 
     def _urlconf_teardown(self):
     def _urlconf_teardown(self):
         set_urlconf(None)
         set_urlconf(None)
@@ -233,6 +239,13 @@ class SimpleTestCase(unittest.TestCase):
         """
         """
         return override_settings(**kwargs)
         return override_settings(**kwargs)
 
 
+    def modify_settings(self, **kwargs):
+        """
+        A context manager that temporarily applies changes a list setting and
+        reverts back to the original value when exiting the context.
+        """
+        return modify_settings(**kwargs)
+
     def assertRedirects(self, response, expected_url, status_code=302,
     def assertRedirects(self, response, expected_url, status_code=302,
                         target_status_code=200, host=None, msg_prefix='',
                         target_status_code=200, host=None, msg_prefix='',
                         fetch_redirect_response=True):
                         fetch_redirect_response=True):

+ 63 - 12
django/test/utils.py

@@ -24,8 +24,10 @@ from django.utils.translation import deactivate
 
 
 
 
 __all__ = (
 __all__ = (
-    'Approximate', 'ContextList', 'get_runner', 'override_settings',
-    'requires_tz_support', 'setup_test_environment', 'teardown_test_environment',
+    'Approximate', 'ContextList', 'get_runner',
+    'modify_settings', 'override_settings',
+    'requires_tz_support',
+    'setup_test_environment', 'teardown_test_environment',
 )
 )
 
 
 RESTORE_LOADERS_ATTR = '_original_template_source_loaders'
 RESTORE_LOADERS_ATTR = '_original_template_source_loaders'
@@ -191,8 +193,6 @@ class override_settings(object):
     """
     """
     def __init__(self, **kwargs):
     def __init__(self, **kwargs):
         self.options = kwargs
         self.options = kwargs
-        # Special case that requires updating the app cache, a core feature.
-        self.installed_apps = self.options.get('INSTALLED_APPS')
 
 
     def __enter__(self):
     def __enter__(self):
         self.enable()
         self.enable()
@@ -207,11 +207,7 @@ class override_settings(object):
                 raise Exception(
                 raise Exception(
                     "Only subclasses of Django SimpleTestCase can be decorated "
                     "Only subclasses of Django SimpleTestCase can be decorated "
                     "with override_settings")
                     "with override_settings")
-            if test_func._custom_settings:
-                test_func._custom_settings = dict(
-                    test_func._custom_settings, **self.options)
-            else:
-                test_func._custom_settings = self.options
+            self.save_options(test_func)
             return test_func
             return test_func
         else:
         else:
             @wraps(test_func)
             @wraps(test_func)
@@ -220,14 +216,22 @@ class override_settings(object):
                     return test_func(*args, **kwargs)
                     return test_func(*args, **kwargs)
         return inner
         return inner
 
 
+    def save_options(self, test_func):
+        if test_func._overridden_settings is None:
+            test_func._overridden_settings = self.options
+        else:
+            # Duplicate dict to prevent subclasses from altering their parent.
+            test_func._overridden_settings = dict(
+                test_func._overridden_settings, **self.options)
+
     def enable(self):
     def enable(self):
         override = UserSettingsHolder(settings._wrapped)
         override = UserSettingsHolder(settings._wrapped)
         for key, new_value in self.options.items():
         for key, new_value in self.options.items():
             setattr(override, key, new_value)
             setattr(override, key, new_value)
         self.wrapped = settings._wrapped
         self.wrapped = settings._wrapped
         settings._wrapped = override
         settings._wrapped = override
-        if self.installed_apps is not None:
-            app_cache.set_installed_apps(self.installed_apps)
+        if 'INSTALLED_APPS' in self.options:
+            app_cache.set_installed_apps(settings.INSTALLED_APPS)
         for key, new_value in self.options.items():
         for key, new_value in self.options.items():
             setting_changed.send(sender=settings._wrapped.__class__,
             setting_changed.send(sender=settings._wrapped.__class__,
                                  setting=key, value=new_value, enter=True)
                                  setting=key, value=new_value, enter=True)
@@ -235,7 +239,7 @@ class override_settings(object):
     def disable(self):
     def disable(self):
         settings._wrapped = self.wrapped
         settings._wrapped = self.wrapped
         del self.wrapped
         del self.wrapped
-        if self.installed_apps is not None:
+        if 'INSTALLED_APPS' in self.options:
             app_cache.unset_installed_apps()
             app_cache.unset_installed_apps()
         for key in self.options:
         for key in self.options:
             new_value = getattr(settings, key, None)
             new_value = getattr(settings, key, None)
@@ -243,6 +247,53 @@ class override_settings(object):
                                  setting=key, value=new_value, enter=False)
                                  setting=key, value=new_value, enter=False)
 
 
 
 
+class modify_settings(override_settings):
+    """
+    Like override_settings, but makes it possible to append, prepend or remove
+    items instead of redefining the entire list.
+    """
+    def __init__(self, *args, **kwargs):
+        if args:
+            # Hack used when instaciating from SimpleTestCase._pre_setup.
+            assert not kwargs
+            self.operations = args[0]
+        else:
+            assert not args
+            self.operations = list(kwargs.items())
+
+    def save_options(self, test_func):
+        if test_func._modified_settings is None:
+            test_func._modified_settings = self.operations
+        else:
+            # Duplicate list to prevent subclasses from altering their parent.
+            test_func._modified_settings = list(
+                test_func._modified_settings) + self.operations
+
+    def enable(self):
+        self.options = {}
+        for name, operations in self.operations:
+            try:
+                # When called from SimpleTestCase._pre_setup, values may be
+                # overridden several times; cumulate changes.
+                value = self.options[name]
+            except KeyError:
+                value = list(getattr(settings, name, []))
+            for action, items in operations.items():
+                # items my be a single value or an iterable.
+                if isinstance(items, six.string_types):
+                    items = [items]
+                if action == 'append':
+                    value = value + [item for item in items if item not in value]
+                elif action == 'prepend':
+                    value = [item for item in items if item not in value] + value
+                elif action == 'remove':
+                    value = [item for item in value if item not in items]
+                else:
+                    raise ValueError("Unsupported action: %s" % action)
+            self.options[name] = value
+        super(modify_settings, self).enable()
+
+
 def compare_xml(want, got):
 def compare_xml(want, got):
     """Tries to do a 'xml-comparison' of want and got.  Plain string
     """Tries to do a 'xml-comparison' of want and got.  Plain string
     comparison doesn't always work because, for example, attribute
     comparison doesn't always work because, for example, attribute

+ 86 - 24
docs/topics/testing/overview.txt

@@ -1335,7 +1335,7 @@ Overriding settings
 
 
 For testing purposes it's often useful to change a setting temporarily and
 For testing purposes it's often useful to change a setting temporarily and
 revert to the original value after running the testing code. For this use case
 revert to the original value after running the testing code. For this use case
-Django provides a standard Python context manager (see :pep:`343`)
+Django provides a standard Python context manager (see :pep:`343`) called
 :meth:`~django.test.SimpleTestCase.settings`, which can be used like this::
 :meth:`~django.test.SimpleTestCase.settings`, which can be used like this::
 
 
     from django.test import TestCase
     from django.test import TestCase
@@ -1356,12 +1356,41 @@ Django provides a standard Python context manager (see :pep:`343`)
 This example will override the :setting:`LOGIN_URL` setting for the code
 This example will override the :setting:`LOGIN_URL` setting for the code
 in the ``with`` block and reset its value to the previous state afterwards.
 in the ``with`` block and reset its value to the previous state afterwards.
 
 
+.. method:: SimpleTestCase.modify_settings
+
+.. versionadded:: 1.7
+
+It can prove unwieldy to redefine settings that contain a list of values. In
+practice, adding or removing values is often sufficient. The
+:meth:`~django.test.SimpleTestCase.modify_settings` context manager makes it
+easy::
+
+    from django.test import TestCase
+
+    class MiddlewareTestCase(TestCase):
+
+        def test_cache_middleware(self):
+            with self.modify_settings(MIDDLEWARE_CLASSES={
+                'append': 'django.middleware.cache.FetchFromCacheMiddleware',
+                'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
+                'remove': [
+                    'django.contrib.sessions.middleware.SessionMiddleware',
+                    'django.contrib.auth.middleware.AuthenticationMiddleware',
+                    'django.contrib.messages.middleware.MessageMiddleware',
+                ],
+            }):
+                response = self.client.get('/')
+                # ...
+
+For each action, you can supply either a list of values or a string. When the
+value already exists in the list, ``append`` and ``prepend`` have no effect;
+neither does ``remove`` when the value doesn't exist.
+
 .. function:: override_settings
 .. function:: override_settings
 
 
-In case you want to override a setting for just one test method or even the
-whole :class:`~django.test.TestCase` class, Django provides the
-:func:`~django.test.override_settings` decorator (see :pep:`318`). It's
-used like this::
+In case you want to override a setting for a test method, Django provides the
+:func:`~django.test.override_settings` decorator (see :pep:`318`). It's used
+like this::
 
 
     from django.test import TestCase, override_settings
     from django.test import TestCase, override_settings
 
 
@@ -1372,7 +1401,7 @@ used like this::
             response = self.client.get('/sekrit/')
             response = self.client.get('/sekrit/')
             self.assertRedirects(response, '/other/login/?next=/sekrit/')
             self.assertRedirects(response, '/other/login/?next=/sekrit/')
 
 
-The decorator can also be applied to test case classes::
+The decorator can also be applied to :class:`~django.test.TestCase` classes::
 
 
     from django.test import TestCase, override_settings
     from django.test import TestCase, override_settings
 
 
@@ -1385,17 +1414,50 @@ The decorator can also be applied to test case classes::
 
 
 .. versionchanged:: 1.7
 .. versionchanged:: 1.7
 
 
-    Previously, ``override_settings`` was imported from
-    ``django.test.utils``.
+    Previously, ``override_settings`` was imported from ``django.test.utils``.
+
+.. function:: modify_settings
+
+.. versionadded:: 1.7
+
+Likewise, Django provides the :func:`~django.test.modify_settings`
+decorator::
+
+    from django.test import TestCase, modify_settings
+
+    class MiddlewareTestCase(TestCase):
+
+        @modify_settings(MIDDLEWARE_CLASSES={
+            'append': 'django.middleware.cache.FetchFromCacheMiddleware',
+            'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
+        })
+        def test_cache_middleware(self):
+            response = self.client.get('/')
+            # ...
+
+The decorator can also be applied to test case classes::
+
+    from django.test import TestCase, modify_settings
+
+    @modify_settings(MIDDLEWARE_CLASSES={
+        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
+        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
+    })
+    class MiddlewareTestCase(TestCase):
+
+        def test_cache_middleware(self):
+            response = self.client.get('/')
+            # ...
 
 
 .. note::
 .. note::
 
 
-    When given a class, the decorator modifies the class directly and
-    returns it; it doesn't create and return a modified copy of it.  So if
-    you try to tweak the above example to assign the return value to a
-    different name than ``LoginTestCase``, you may be surprised to find that
-    the original ``LoginTestCase`` is still equally affected by the
-    decorator.
+    When given a class, these decorators modify the class directly and return
+    it; they don't create and return a modified copy of it. So if you try to
+    tweak the above examples to assign the return value to a different name
+    than ``LoginTestCase`` or ``MiddlewareTestCase``, you may be surprised to
+    find that the original test case classes are still equally affected by the
+    decorator. For a given class, :func:`~django.test.modify_settings` is
+    always applied after :func:`~django.test.override_settings`.
 
 
 .. warning::
 .. warning::
 
 
@@ -1403,17 +1465,17 @@ The decorator can also be applied to test case classes::
     initialization of Django internals. If you change them with
     initialization of Django internals. If you change them with
     ``override_settings``, the setting is changed if you access it via the
     ``override_settings``, the setting is changed if you access it via the
     ``django.conf.settings`` module, however, Django's internals access it
     ``django.conf.settings`` module, however, Django's internals access it
-    differently. Effectively, using ``override_settings`` with these settings
-    is probably not going to do what you expect it to do.
+    differently. Effectively, using :func:`~django.test.override_settings` or
+    :func:`~django.test.modify_settings` with these settings is probably not
+    going to do what you expect it to do.
 
 
-    We do not recommend using ``override_settings`` with :setting:`DATABASES`.
-    Using ``override_settings`` with :setting:`CACHES` is possible, but a bit
-    tricky if you are using internals that make using of caching, like
+    We do not recommend altering the :setting:`DATABASES` setting. Altering
+    the :setting:`CACHES` setting is possible, but a bit tricky if you are
+    using internals that make using of caching, like
     :mod:`django.contrib.sessions`. For example, you will have to reinitialize
     :mod:`django.contrib.sessions`. For example, you will have to reinitialize
     the session backend in a test that uses cached sessions and overrides
     the session backend in a test that uses cached sessions and overrides
     :setting:`CACHES`.
     :setting:`CACHES`.
 
 
-
 You can also simulate the absence of a setting by deleting it after settings
 You can also simulate the absence of a setting by deleting it after settings
 have been overridden, like this::
 have been overridden, like this::
 
 
@@ -1423,10 +1485,10 @@ have been overridden, like this::
         ...
         ...
 
 
 When overriding settings, make sure to handle the cases in which your app's
 When overriding settings, make sure to handle the cases in which your app's
-code uses a cache or similar feature that retains state even if the
-setting is changed. Django provides the
-:data:`django.test.signals.setting_changed` signal that lets you register
-callbacks to clean up and otherwise reset state when settings are changed.
+code uses a cache or similar feature that retains state even if the setting is
+changed. Django provides the :data:`django.test.signals.setting_changed`
+signal that lets you register callbacks to clean up and otherwise reset state
+when settings are changed.
 
 
 Django itself uses this signal to reset various data:
 Django itself uses this signal to reset various data:
 
 

+ 57 - 4
tests/settings_tests/tests.py

@@ -6,19 +6,57 @@ from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.exceptions import ImproperlyConfigured
 from django.http import HttpRequest
 from django.http import HttpRequest
 from django.test import SimpleTestCase, TransactionTestCase, TestCase, signals
 from django.test import SimpleTestCase, TransactionTestCase, TestCase, signals
-from django.test.utils import override_settings
+from django.test.utils import modify_settings, override_settings
 from django.utils import six
 from django.utils import six
 
 
 
 
-@override_settings(TEST='override', TEST_OUTER='outer')
+@modify_settings(ITEMS={
+    'prepend': ['b'],
+    'append': ['d'],
+    'remove': ['a', 'e']
+})
+@override_settings(ITEMS=['a', 'c', 'e'], ITEMS_OUTER=[1, 2, 3],
+                   TEST='override', TEST_OUTER='outer')
 class FullyDecoratedTranTestCase(TransactionTestCase):
 class FullyDecoratedTranTestCase(TransactionTestCase):
 
 
     available_apps = []
     available_apps = []
 
 
     def test_override(self):
     def test_override(self):
+        self.assertListEqual(settings.ITEMS, ['b', 'c', 'd'])
+        self.assertListEqual(settings.ITEMS_OUTER, [1, 2, 3])
         self.assertEqual(settings.TEST, 'override')
         self.assertEqual(settings.TEST, 'override')
         self.assertEqual(settings.TEST_OUTER, 'outer')
         self.assertEqual(settings.TEST_OUTER, 'outer')
 
 
+    @modify_settings(ITEMS={
+        'append': ['e', 'f'],
+        'prepend': ['a'],
+        'remove': ['d', 'c'],
+    })
+    def test_method_list_override(self):
+        self.assertListEqual(settings.ITEMS, ['a', 'b', 'e', 'f'])
+        self.assertListEqual(settings.ITEMS_OUTER, [1, 2, 3])
+
+    @modify_settings(ITEMS={
+        'append': ['b'],
+        'prepend': ['d'],
+        'remove': ['a', 'c', 'e'],
+    })
+    def test_method_list_override_no_ops(self):
+        self.assertListEqual(settings.ITEMS, ['b', 'd'])
+
+    @modify_settings(ITEMS={
+        'append': 'e',
+        'prepend': 'a',
+        'remove': 'c',
+    })
+    def test_method_list_override_strings(self):
+        self.assertListEqual(settings.ITEMS, ['a', 'b', 'd', 'e'])
+
+    @modify_settings(ITEMS={'remove': ['b', 'd']})
+    @modify_settings(ITEMS={'append': ['b'], 'prepend': ['d']})
+    def test_method_list_override_nested_order(self):
+        self.assertListEqual(settings.ITEMS, ['d', 'c', 'b'])
+
     @override_settings(TEST='override2')
     @override_settings(TEST='override2')
     def test_method_override(self):
     def test_method_override(self):
         self.assertEqual(settings.TEST, 'override2')
         self.assertEqual(settings.TEST, 'override2')
@@ -31,14 +69,26 @@ class FullyDecoratedTranTestCase(TransactionTestCase):
         self.assertEqual(FullyDecoratedTranTestCase.__module__, __name__)
         self.assertEqual(FullyDecoratedTranTestCase.__module__, __name__)
 
 
 
 
-@override_settings(TEST='override')
+@modify_settings(ITEMS={
+    'prepend': ['b'],
+    'append': ['d'],
+    'remove': ['a', 'e']
+})
+@override_settings(ITEMS=['a', 'c', 'e'], TEST='override')
 class FullyDecoratedTestCase(TestCase):
 class FullyDecoratedTestCase(TestCase):
 
 
     def test_override(self):
     def test_override(self):
+        self.assertListEqual(settings.ITEMS, ['b', 'c', 'd'])
         self.assertEqual(settings.TEST, 'override')
         self.assertEqual(settings.TEST, 'override')
 
 
+    @modify_settings(ITEMS={
+        'append': 'e',
+        'prepend': 'a',
+        'remove': 'c',
+    })
     @override_settings(TEST='override2')
     @override_settings(TEST='override2')
     def test_method_override(self):
     def test_method_override(self):
+        self.assertListEqual(settings.ITEMS, ['a', 'b', 'd', 'e'])
         self.assertEqual(settings.TEST, 'override2')
         self.assertEqual(settings.TEST, 'override2')
 
 
 
 
@@ -73,14 +123,17 @@ class ClassDecoratedTestCase(ClassDecoratedTestCaseSuper):
             self.fail()
             self.fail()
 
 
 
 
-@override_settings(TEST='override-parent')
+@modify_settings(ITEMS={'append': 'mother'})
+@override_settings(ITEMS=['father'], TEST='override-parent')
 class ParentDecoratedTestCase(TestCase):
 class ParentDecoratedTestCase(TestCase):
     pass
     pass
 
 
 
 
+@modify_settings(ITEMS={'append': ['child']})
 @override_settings(TEST='override-child')
 @override_settings(TEST='override-child')
 class ChildDecoratedTestCase(ParentDecoratedTestCase):
 class ChildDecoratedTestCase(ParentDecoratedTestCase):
     def test_override_settings_inheritance(self):
     def test_override_settings_inheritance(self):
+        self.assertEqual(settings.ITEMS, ['father', 'mother', 'child'])
         self.assertEqual(settings.TEST, 'override-child')
         self.assertEqual(settings.TEST, 'override-child')
 
 
 
 

+ 1 - 0
tests/view_tests/tests/test_i18n.py

@@ -135,6 +135,7 @@ class JsI18NTests(TestCase):
         response = self.client.get('/views/jsi18n_admin/?language=de')
         response = self.client.get('/views/jsi18n_admin/?language=de')
         self.assertContains(response, '\\x04')
         self.assertContains(response, '\\x04')
 
 
+
 class JsI18NTestsMultiPackage(TestCase):
 class JsI18NTestsMultiPackage(TestCase):
     """
     """
     Tests for django views in django/views/i18n.py that need to change
     Tests for django views in django/views/i18n.py that need to change