Pārlūkot izejas kodu

Fixed #15561 -- Extended test setting override code added in r16165 with a decorator and a signal for setting changes.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16237 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Jannis Leidel 14 gadi atpakaļ
vecāks
revīzija
a3a53e0b73

+ 2 - 0
django/test/signals.py

@@ -1,3 +1,5 @@
 from django.dispatch import Signal
 
 template_rendered = Signal(providing_args=["template", "context"])
+
+setting_changed = Signal(providing_args=["setting", "value"])

+ 3 - 13
django/test/testcases.py

@@ -2,7 +2,6 @@ from __future__ import with_statement
 
 import re
 import sys
-from contextlib import contextmanager
 from functools import wraps
 from urlparse import urlsplit, urlunsplit
 from xml.dom.minidom import parseString, Node
@@ -17,7 +16,7 @@ from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
 from django.http import QueryDict
 from django.test import _doctest as doctest
 from django.test.client import Client
-from django.test.utils import get_warnings_state, restore_warnings_state
+from django.test.utils import get_warnings_state, restore_warnings_state, override_settings
 from django.utils import simplejson, unittest as ut2
 from django.utils.encoding import smart_str
 
@@ -342,21 +341,12 @@ class TransactionTestCase(ut2.TestCase):
         """
         restore_warnings_state(self._warnings_state)
 
-    @contextmanager
-    def settings(self, **options):
+    def settings(self, **kwargs):
         """
         A context manager that temporarily sets a setting and reverts
         back to the original value when exiting the context.
         """
-        old_wrapped = settings._wrapped
-        override = UserSettingsHolder(settings._wrapped)
-        try:
-            for key, new_value in options.items():
-                setattr(override, key, new_value)
-            settings._wrapped = override
-            yield
-        finally:
-            settings._wrapped = old_wrapped
+        return override_settings(**kwargs)
 
     def assertRedirects(self, response, expected_url, status_code=302,
                         target_status_code=200, host=None, msg_prefix=''):

+ 54 - 5
django/test/utils.py

@@ -1,17 +1,23 @@
+from __future__ import with_statement
+
 import sys
 import time
 import os
 import warnings
-from django.conf import settings
+from django.conf import settings, UserSettingsHolder
 from django.core import mail
 from django.core.mail.backends import locmem
-from django.test import signals
+from django.test.signals import template_rendered, setting_changed
 from django.template import Template, loader, TemplateDoesNotExist
 from django.template.loaders import cached
 from django.utils.translation import deactivate
+from django.utils.functional import wraps
+
 
-__all__ = ('Approximate', 'ContextList', 'setup_test_environment',
-       'teardown_test_environment', 'get_runner')
+__all__ = (
+    'Approximate', 'ContextList',  'get_runner', 'override_settings',
+    'setup_test_environment', 'teardown_test_environment',
+)
 
 RESTORE_LOADERS_ATTR = '_original_template_source_loaders'
 
@@ -56,7 +62,7 @@ def instrumented_test_render(self, context):
     An instrumented Template render method, providing a signal
     that can be intercepted by the test system Client
     """
-    signals.template_rendered.send(sender=self, template=self, context=context)
+    template_rendered.send(sender=self, template=self, context=context)
     return self.nodelist.render(context)
 
 
@@ -160,3 +166,46 @@ def restore_template_loaders():
     """
     loader.template_source_loaders = getattr(loader, RESTORE_LOADERS_ATTR)
     delattr(loader, RESTORE_LOADERS_ATTR)
+
+
+class OverrideSettingsHolder(UserSettingsHolder):
+    """
+    A custom setting holder that sends a signal upon change.
+    """
+    def __setattr__(self, name, value):
+        UserSettingsHolder.__setattr__(self, name, value)
+        setting_changed.send(sender=name, setting=name, value=value)
+
+
+class override_settings(object):
+    """
+    Acts as either a decorator, or a context manager. If it's a decorator it
+    takes a function and returns a wrapped function. If it's a contextmanager
+    it's used with the ``with`` statement. In either event entering/exiting
+    are called before and after, respectively, the function/block is executed.
+    """
+    def __init__(self, **kwargs):
+        self.options = kwargs
+        self.wrapped = settings._wrapped
+
+    def __enter__(self):
+        self.enable()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.disable()
+
+    def __call__(self, func):
+        @wraps(func)
+        def inner(*args, **kwargs):
+            with self:
+                return func(*args, **kwargs)
+        return inner
+
+    def enable(self):
+        override = OverrideSettingsHolder(settings._wrapped)
+        for key, new_value in self.options.items():
+            setattr(override, key, new_value)
+        settings._wrapped = override
+
+    def disable(self):
+        settings._wrapped = self.wrapped

+ 23 - 0
docs/ref/signals.txt

@@ -460,6 +460,29 @@ Test signals
 
 Signals only sent when :doc:`running tests </topics/testing>`.
 
+setting_changed
+---------------
+
+.. versionadded:: 1.4
+
+.. data:: django.test.signals.setting_changed
+   :module:
+
+Sent when some :ref:`settings are overridden <overriding-setting>` with the
+:meth:`django.test.TestCase.setting` context manager or the
+:func:`django.test.utils.override_settings` decorator/context manager.
+
+Arguments sent with this signal:
+
+``sender``
+    The setting name (string).
+
+``setting``
+    Same as sender
+
+``value``
+    The new setting value.
+
 template_rendered
 -----------------
 

+ 61 - 1
docs/topics/testing.txt

@@ -1361,6 +1361,8 @@ For example::
 This test case will flush *all* the test databases before running
 ``testIndexPageView``.
 
+.. _overriding-setting:
+
 Overriding settings
 ~~~~~~~~~~~~~~~~~~~
 
@@ -1376,7 +1378,14 @@ this use case Django provides a standard `Python context manager`_
     from django.test import TestCase
 
     class LoginTestCase(TestCase):
-        def test_overriding_settings(self):
+
+        def test_login(self):
+
+            # First check for the default behavior
+            response = self.client.get('/sekrit/')
+            self.assertRedirects(response, '/accounts/login/?next=/sekrit/')
+
+            # Then override the LOGING_URL setting
             with self.settings(LOGIN_URL='/other/login/'):
                 response = self.client.get('/sekrit/')
                 self.assertRedirects(response, '/other/login/?next=/sekrit/')
@@ -1384,7 +1393,58 @@ this use case Django provides a standard `Python context manager`_
 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.
 
+.. function:: utils.override_settings
+
+In case you want to override a setting for just one test method or even the
+whole TestCase class, Django provides the
+:func:`django.test.utils.override_settings` decorator_. It's used like this::
+
+    from django.test import TestCase
+    from django.test.utils import override_settings
+
+    class LoginTestCase(TestCase):
+
+        @override_settings(LOGIN_URL='/other/login/')
+        def test_login(self):
+            response = self.client.get('/sekrit/')
+            self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+The decorator can also be applied to test case classes::
+
+    from django.test import TestCase
+    from django.test.utils import override_settings
+
+    class LoginTestCase(TestCase):
+
+        def test_login(self):
+            response = self.client.get('/sekrit/')
+            self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+    LoginTestCase = override_settings(LOGIN_URL='/other/login/')(LoginTestCase)
+
+On Python 2.6 and higher you can also use the well known decorator syntax to
+decorate the class::
+
+    from django.test import TestCase
+    from django.test.utils import override_settings
+
+    @override_settings(LOGIN_URL='/other/login/')
+    class LoginTestCase(TestCase):
+
+        def test_login(self):
+            response = self.client.get('/sekrit/')
+            self.assertRedirects(response, '/other/login/?next=/sekrit/')
+
+.. note::
+
+    When overriding settings make sure to also handle the cases in which
+    Django or your app's code use a cache or another feature that retain
+    state even if the setting is changed. Django provides the
+    :data:`django.test.signals.setting_changed` signal to connect cleanup
+    and other state resetting callbacks to.
+
 .. _`Python context manager`: http://www.python.org/dev/peps/pep-0343/
+.. _`decorator`: http://www.python.org/dev/peps/pep-0318/
 
 Emptying the test outbox
 ~~~~~~~~~~~~~~~~~~~~~~~~

+ 19 - 48
tests/regressiontests/mail/tests.py

@@ -9,48 +9,14 @@ from StringIO import StringIO
 import tempfile
 import threading
 
-from django.conf import settings
 from django.core import mail
 from django.core.mail import (EmailMessage, mail_admins, mail_managers,
         EmailMultiAlternatives, send_mail, send_mass_mail)
 from django.core.mail.backends import console, dummy, locmem, filebased, smtp
 from django.core.mail.message import BadHeaderError
 from django.test import TestCase
+from django.test.utils import override_settings
 from django.utils.translation import ugettext_lazy
-from django.utils.functional import wraps
-
-
-def alter_django_settings(**kwargs):
-    oldvalues = {}
-    nonexistant = []
-    for setting, newvalue in kwargs.iteritems():
-        try:
-            oldvalues[setting] = getattr(settings, setting)
-        except AttributeError:
-            nonexistant.append(setting)
-        setattr(settings, setting, newvalue)
-    return oldvalues, nonexistant
-
-
-def restore_django_settings(state):
-    oldvalues, nonexistant = state
-    for setting, oldvalue in oldvalues.iteritems():
-        setattr(settings, setting, oldvalue)
-    for setting in nonexistant:
-        delattr(settings, setting)
-
-
-def with_django_settings(**kwargs):
-    def decorator(test):
-        @wraps(test)
-        def decorated_test(self):
-            state = alter_django_settings(**kwargs)
-            try:
-                return test(self)
-            finally:
-                restore_django_settings(state)
-        return decorated_test
-    return decorator
 
 
 class MailTests(TestCase):
@@ -251,7 +217,7 @@ class MailTests(TestCase):
             shutil.rmtree(tmp_dir)
         self.assertTrue(isinstance(mail.get_connection(), locmem.EmailBackend))
 
-    @with_django_settings(
+    @override_settings(
         EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',
         ADMINS=[('nobody', 'nobody@example.com')],
         MANAGERS=[('nobody', 'nobody@example.com')])
@@ -323,10 +289,11 @@ class BaseEmailBackendTests(object):
     email_backend = None
 
     def setUp(self):
-        self.__settings_state = alter_django_settings(EMAIL_BACKEND=self.email_backend)
+        self.settings_override = override_settings(EMAIL_BACKEND=self.email_backend)
+        self.settings_override.enable()
 
     def tearDown(self):
-        restore_django_settings(self.__settings_state)
+        self.settings_override.disable()
 
     def assertStartsWith(self, first, second):
         if not first.startswith(second):
@@ -375,7 +342,7 @@ class BaseEmailBackendTests(object):
         self.assertEqual(message.get_payload(), "Content")
         self.assertEqual(message["from"], "=?utf-8?q?Firstname_S=C3=BCrname?= <from@example.com>")
 
-    @with_django_settings(MANAGERS=[('nobody', 'nobody@example.com')])
+    @override_settings(MANAGERS=[('nobody', 'nobody@example.com')])
     def test_html_mail_managers(self):
         """Test html_message argument to mail_managers"""
         mail_managers('Subject', 'Content', html_message='HTML Content')
@@ -390,7 +357,7 @@ class BaseEmailBackendTests(object):
         self.assertEqual(message.get_payload(1).get_payload(), 'HTML Content')
         self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
 
-    @with_django_settings(ADMINS=[('nobody', 'nobody@example.com')])
+    @override_settings(ADMINS=[('nobody', 'nobody@example.com')])
     def test_html_mail_admins(self):
         """Test html_message argument to mail_admins """
         mail_admins('Subject', 'Content', html_message='HTML Content')
@@ -405,8 +372,9 @@ class BaseEmailBackendTests(object):
         self.assertEqual(message.get_payload(1).get_payload(), 'HTML Content')
         self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
 
-    @with_django_settings(ADMINS=[('nobody', 'nobody+admin@example.com')],
-                         MANAGERS=[('nobody', 'nobody+manager@example.com')])
+    @override_settings(
+        ADMINS=[('nobody', 'nobody+admin@example.com')],
+        MANAGERS=[('nobody', 'nobody+manager@example.com')])
     def test_manager_and_admin_mail_prefix(self):
         """
         String prefix + lazy translated subject = bad output
@@ -421,7 +389,7 @@ class BaseEmailBackendTests(object):
         message = self.get_the_message()
         self.assertEqual(message.get('subject'), '[Django] Subject')
 
-    @with_django_settings(ADMINS=(), MANAGERS=())
+    @override_settings(ADMINS=(), MANAGERS=())
     def test_empty_admins(self):
         """
         Test that mail_admins/mail_managers doesn't connect to the mail server
@@ -501,13 +469,14 @@ class FileBackendTests(BaseEmailBackendTests, TestCase):
     email_backend = 'django.core.mail.backends.filebased.EmailBackend'
 
     def setUp(self):
-        super(FileBackendTests, self).setUp()
         self.tmp_dir = tempfile.mkdtemp()
-        self.__settings_state = alter_django_settings(EMAIL_FILE_PATH=self.tmp_dir)
+        self.addCleanup(shutil.rmtree, self.tmp_dir)
+        self.settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir)
+        self.settings_override.enable()
+        super(FileBackendTests, self).setUp()
 
     def tearDown(self):
-        restore_django_settings(self.__settings_state)
-        shutil.rmtree(self.tmp_dir)
+        self.settings_override.disable()
         super(FileBackendTests, self).tearDown()
 
     def flush_mailbox(self):
@@ -642,13 +611,15 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase):
     @classmethod
     def setUpClass(cls):
         cls.server = FakeSMTPServer(('127.0.0.1', 0), None)
-        cls.settings = alter_django_settings(
+        cls.settings_override = override_settings(
             EMAIL_HOST="127.0.0.1",
             EMAIL_PORT=cls.server.socket.getsockname()[1])
+        cls.settings_override.enable()
         cls.server.start()
 
     @classmethod
     def tearDownClass(cls):
+        cls.settings_override.disable()
         cls.server.stop()
 
     def setUp(self):

+ 54 - 2
tests/regressiontests/settings_tests/tests.py

@@ -1,7 +1,22 @@
 from __future__ import with_statement
-import os
+import os, sys
 from django.conf import settings, global_settings
-from django.test import TestCase
+from django.test import TestCase, signals
+from django.test.utils import override_settings
+from django.utils.unittest import skipIf
+
+
+class SettingGetter(object):
+    def __init__(self):
+        self.test = getattr(settings, 'TEST', 'undefined')
+
+testvalue = None
+
+def signal_callback(sender, setting, value, **kwargs):
+    global testvalue
+    testvalue = value
+
+signals.setting_changed.connect(signal_callback, sender='TEST')
 
 class SettingsTests(TestCase):
 
@@ -29,6 +44,43 @@ class SettingsTests(TestCase):
             settings.TEST = 'test'
         self.assertRaises(AttributeError, getattr, settings, 'TEST')
 
+    @override_settings(TEST='override')
+    def test_decorator(self):
+        self.assertEqual('override', settings.TEST)
+
+    def test_context_manager(self):
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+        override = override_settings(TEST='override')
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+        override.enable()
+        self.assertEqual('override', settings.TEST)
+        override.disable()
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+    def test_class_decorator(self):
+        self.assertEqual(SettingGetter().test, 'undefined')
+        DecoratedSettingGetter = override_settings(TEST='override')(SettingGetter)
+        self.assertEqual(DecoratedSettingGetter().test, 'override')
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+    @skipIf(sys.version_info[:2] < (2, 6), "Python version is lower than 2.6")
+    def test_new_class_decorator(self):
+        self.assertEqual(SettingGetter().test, 'undefined')
+        @override_settings(TEST='override')
+        class DecoratedSettingGetter(SettingGetter):
+            pass
+        self.assertEqual(DecoratedSettingGetter().test, 'override')
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+
+    def test_signal_callback_context_manager(self):
+        self.assertRaises(AttributeError, getattr, settings, 'TEST')
+        with self.settings(TEST='override'):
+            self.assertEqual(testvalue, 'override')
+
+    @override_settings(TEST='override')
+    def test_signal_callback_decorator(self):
+        self.assertEqual(testvalue, 'override')
+
     #
     # Regression tests for #10130: deleting settings.
     #