Added modify_settings to alter settings containing lists of values.
This commit is contained in:
parent
5891990b6e
commit
5241763c81
|
@ -8,10 +8,10 @@ from django.test.testcases import (
|
|||
SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
|
||||
skipUnlessDBFeature
|
||||
)
|
||||
from django.test.utils import override_settings
|
||||
from django.test.utils import modify_settings, override_settings
|
||||
|
||||
__all__ = [
|
||||
'Client', 'RequestFactory', 'TestCase', 'TransactionTestCase',
|
||||
'SimpleTestCase', 'LiveServerTestCase', 'skipIfDBFeature',
|
||||
'skipUnlessDBFeature', 'override_settings',
|
||||
'skipUnlessDBFeature', 'modify_settings', 'override_settings',
|
||||
]
|
||||
|
|
|
@ -32,7 +32,7 @@ from django.test.client import Client
|
|||
from django.test.html import HTMLParseError, parse_html
|
||||
from django.test.signals import setting_changed, template_rendered
|
||||
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 import six
|
||||
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.
|
||||
# Can be overridden in derived classes.
|
||||
client_class = Client
|
||||
_custom_settings = None
|
||||
_overridden_settings = None
|
||||
_modified_settings = 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.
|
||||
* 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._urlconf_setup()
|
||||
mail.outbox = []
|
||||
|
@ -217,8 +221,10 @@ class SimpleTestCase(unittest.TestCase):
|
|||
* Putting back the original ROOT_URLCONF if it was changed.
|
||||
"""
|
||||
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):
|
||||
set_urlconf(None)
|
||||
|
@ -233,6 +239,13 @@ class SimpleTestCase(unittest.TestCase):
|
|||
"""
|
||||
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,
|
||||
target_status_code=200, host=None, msg_prefix='',
|
||||
fetch_redirect_response=True):
|
||||
|
|
|
@ -24,8 +24,10 @@ from django.utils.translation import deactivate
|
|||
|
||||
|
||||
__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'
|
||||
|
@ -191,8 +193,6 @@ class override_settings(object):
|
|||
"""
|
||||
def __init__(self, **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):
|
||||
self.enable()
|
||||
|
@ -207,11 +207,7 @@ class override_settings(object):
|
|||
raise Exception(
|
||||
"Only subclasses of Django SimpleTestCase can be decorated "
|
||||
"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
|
||||
else:
|
||||
@wraps(test_func)
|
||||
|
@ -220,14 +216,22 @@ class override_settings(object):
|
|||
return test_func(*args, **kwargs)
|
||||
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):
|
||||
override = UserSettingsHolder(settings._wrapped)
|
||||
for key, new_value in self.options.items():
|
||||
setattr(override, key, new_value)
|
||||
self.wrapped = settings._wrapped
|
||||
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():
|
||||
setting_changed.send(sender=settings._wrapped.__class__,
|
||||
setting=key, value=new_value, enter=True)
|
||||
|
@ -235,7 +239,7 @@ class override_settings(object):
|
|||
def disable(self):
|
||||
settings._wrapped = self.wrapped
|
||||
del self.wrapped
|
||||
if self.installed_apps is not None:
|
||||
if 'INSTALLED_APPS' in self.options:
|
||||
app_cache.unset_installed_apps()
|
||||
for key in self.options:
|
||||
new_value = getattr(settings, key, None)
|
||||
|
@ -243,6 +247,53 @@ class override_settings(object):
|
|||
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):
|
||||
"""Tries to do a 'xml-comparison' of want and got. Plain string
|
||||
comparison doesn't always work because, for example, attribute
|
||||
|
|
|
@ -1335,7 +1335,7 @@ Overriding settings
|
|||
|
||||
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
|
||||
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::
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -1372,7 +1401,7 @@ used like this::
|
|||
response = self.client.get('/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
|
||||
|
||||
|
@ -1385,17 +1414,50 @@ The decorator can also be applied to test case classes::
|
|||
|
||||
.. 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::
|
||||
|
||||
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::
|
||||
|
||||
|
@ -1403,17 +1465,17 @@ The decorator can also be applied to test case classes::
|
|||
initialization of Django internals. If you change them with
|
||||
``override_settings``, the setting is changed if you access it via the
|
||||
``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
|
||||
the session backend in a test that uses cached sessions and overrides
|
||||
:setting:`CACHES`.
|
||||
|
||||
|
||||
You can also simulate the absence of a setting by deleting it after settings
|
||||
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
|
||||
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:
|
||||
|
||||
|
|
|
@ -6,19 +6,57 @@ from django.conf import settings
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest
|
||||
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
|
||||
|
||||
|
||||
@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):
|
||||
|
||||
available_apps = []
|
||||
|
||||
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_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')
|
||||
def test_method_override(self):
|
||||
self.assertEqual(settings.TEST, 'override2')
|
||||
|
@ -31,14 +69,26 @@ class FullyDecoratedTranTestCase(TransactionTestCase):
|
|||
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):
|
||||
|
||||
def test_override(self):
|
||||
self.assertListEqual(settings.ITEMS, ['b', 'c', 'd'])
|
||||
self.assertEqual(settings.TEST, 'override')
|
||||
|
||||
@modify_settings(ITEMS={
|
||||
'append': 'e',
|
||||
'prepend': 'a',
|
||||
'remove': 'c',
|
||||
})
|
||||
@override_settings(TEST='override2')
|
||||
def test_method_override(self):
|
||||
self.assertListEqual(settings.ITEMS, ['a', 'b', 'd', 'e'])
|
||||
self.assertEqual(settings.TEST, 'override2')
|
||||
|
||||
|
||||
|
@ -73,14 +123,17 @@ class ClassDecoratedTestCase(ClassDecoratedTestCaseSuper):
|
|||
self.fail()
|
||||
|
||||
|
||||
@override_settings(TEST='override-parent')
|
||||
@modify_settings(ITEMS={'append': 'mother'})
|
||||
@override_settings(ITEMS=['father'], TEST='override-parent')
|
||||
class ParentDecoratedTestCase(TestCase):
|
||||
pass
|
||||
|
||||
|
||||
@modify_settings(ITEMS={'append': ['child']})
|
||||
@override_settings(TEST='override-child')
|
||||
class ChildDecoratedTestCase(ParentDecoratedTestCase):
|
||||
def test_override_settings_inheritance(self):
|
||||
self.assertEqual(settings.ITEMS, ['father', 'mother', 'child'])
|
||||
self.assertEqual(settings.TEST, 'override-child')
|
||||
|
||||
|
||||
|
|
|
@ -135,6 +135,7 @@ class JsI18NTests(TestCase):
|
|||
response = self.client.get('/views/jsi18n_admin/?language=de')
|
||||
self.assertContains(response, '\\x04')
|
||||
|
||||
|
||||
class JsI18NTestsMultiPackage(TestCase):
|
||||
"""
|
||||
Tests for django views in django/views/i18n.py that need to change
|
||||
|
|
Loading…
Reference in New Issue