Added modify_settings to alter settings containing lists of values.

This commit is contained in:
Aymeric Augustin 2013-12-23 12:39:19 +01:00
parent 5891990b6e
commit 5241763c81
6 changed files with 229 additions and 49 deletions

View File

@ -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',
] ]

View File

@ -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: if self._overridden_settings:
self._overridden = override_settings(**self._custom_settings) self._overridden_context = override_settings(**self._overridden_settings)
self._overridden.enable() 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: if self._modified_settings:
self._overridden.disable() 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):

View File

@ -24,8 +24,10 @@ from django.utils.translation import deactivate
__all__ = ( __all__ = (
'Approximate', 'ContextList', 'get_runner', 'override_settings', 'Approximate', 'ContextList', 'get_runner',
'requires_tz_support', 'setup_test_environment', 'teardown_test_environment', '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: self.save_options(test_func)
test_func._custom_settings = dict(
test_func._custom_settings, **self.options)
else:
test_func._custom_settings = self.options
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: if 'INSTALLED_APPS' in self.options:
app_cache.set_installed_apps(self.installed_apps) 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

View File

@ -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 In case you want to override a setting for a test method, Django provides the
whole :class:`~django.test.TestCase` class, Django provides the :func:`~django.test.override_settings` decorator (see :pep:`318`). It's used
:func:`~django.test.override_settings` decorator (see :pep:`318`). It's like this::
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 Previously, ``override_settings`` was imported from ``django.test.utils``.
``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 When given a class, these decorators modify the class directly and return
returns it; it doesn't create and return a modified copy of it. So if it; they don't create and return a modified copy of it. So if you try to
you try to tweak the above example to assign the return value to a tweak the above examples to assign the return value to a different name
different name than ``LoginTestCase``, you may be surprised to find that than ``LoginTestCase`` or ``MiddlewareTestCase``, you may be surprised to
the original ``LoginTestCase`` is still equally affected by the find that the original test case classes are still equally affected by the
decorator. 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 differently. Effectively, using :func:`~django.test.override_settings` or
is probably not going to do what you expect it to do. :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`. We do not recommend altering the :setting:`DATABASES` setting. Altering
Using ``override_settings`` with :setting:`CACHES` is possible, but a bit the :setting:`CACHES` setting is possible, but a bit tricky if you are
tricky if you are using internals that make using of caching, like 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 code uses a cache or similar feature that retains state even if the setting is
setting is changed. Django provides the changed. Django provides the :data:`django.test.signals.setting_changed`
:data:`django.test.signals.setting_changed` signal that lets you register signal that lets you register callbacks to clean up and otherwise reset state
callbacks to clean up and otherwise reset state when settings are changed. when settings are changed.
Django itself uses this signal to reset various data: Django itself uses this signal to reset various data:

View File

@ -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')

View File

@ -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