From a3a53e0b7364c7f9bf3a2f1551dc527505051974 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 18 May 2011 12:08:53 +0000 Subject: [PATCH] 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 --- django/test/signals.py | 2 + django/test/testcases.py | 16 +---- django/test/utils.py | 59 ++++++++++++++-- docs/ref/signals.txt | 23 +++++++ docs/topics/testing.txt | 62 ++++++++++++++++- tests/regressiontests/mail/tests.py | 67 ++++++------------- tests/regressiontests/settings_tests/tests.py | 56 +++++++++++++++- 7 files changed, 216 insertions(+), 69 deletions(-) diff --git a/django/test/signals.py b/django/test/signals.py index a328a7782e..e29f4704bb 100644 --- a/django/test/signals.py +++ b/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"]) diff --git a/django/test/testcases.py b/django/test/testcases.py index d85bc27218..bfa1aeb219 100644 --- a/django/test/testcases.py +++ b/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=''): diff --git a/django/test/utils.py b/django/test/utils.py index bf1dc4f165..c394dacfd6 100644 --- a/django/test/utils.py +++ b/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 diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 4cacdcb301..67476a072f 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -460,6 +460,29 @@ Test signals Signals only sent when :doc:`running tests `. +setting_changed +--------------- + +.. versionadded:: 1.4 + +.. data:: django.test.signals.setting_changed + :module: + +Sent when some :ref:`settings are overridden ` 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 ----------------- diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 61e860c632..36045d2fd7 100644 --- a/docs/topics/testing.txt +++ b/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 ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 706b3afff1..5d5f52b6cd 100644 --- a/tests/regressiontests/mail/tests.py +++ b/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?= ") - @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): diff --git a/tests/regressiontests/settings_tests/tests.py b/tests/regressiontests/settings_tests/tests.py index 750ccc604e..cb2f9e0f1a 100644 --- a/tests/regressiontests/settings_tests/tests.py +++ b/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. #