From 90ecffae08bef8d19a15ac2a7680a42f659ceffd Mon Sep 17 00:00:00 2001 From: jihtsan <15151999611@163.com> Date: Mon, 26 Oct 2026 10:14:57 +0800 Subject: [PATCH 1/2] Support asynchronous override_config decorators (#522) --- constance/test/unittest.py | 22 ++++++++++++--- docs/testing.rst | 5 ++++ tests/test_test_overrides.py | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/constance/test/unittest.py b/constance/test/unittest.py index 779300cb..2b7977b7 100644 --- a/constance/test/unittest.py +++ b/constance/test/unittest.py @@ -1,5 +1,7 @@ from functools import wraps +from asgiref.sync import iscoroutinefunction +from asgiref.sync import sync_to_async from django import VERSION as DJANGO_VERSION from django.test import SimpleTestCase from django.test.utils import override_settings @@ -27,10 +29,22 @@ def __call__(self, test_func): raise Exception("Only subclasses of Django SimpleTestCase can be decorated with override_config") return self.modify_test_case(test_func) - @wraps(test_func) - def inner(*args, **kwargs): - with self: - return test_func(*args, **kwargs) + if iscoroutinefunction(test_func): + + @wraps(test_func) + async def inner(*args, **kwargs): + await sync_to_async(self.enable, thread_sensitive=True)() + try: + return await test_func(*args, **kwargs) + finally: + await sync_to_async(self.disable, thread_sensitive=True)() + + else: + + @wraps(test_func) + def inner(*args, **kwargs): + with self: + return test_func(*args, **kwargs) return inner diff --git a/docs/testing.rst b/docs/testing.rst index 1db02308..7f47651a 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -40,6 +40,11 @@ method level and also as a self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?") +The method decorator also supports ``async def`` tests. The overridden values +remain active until the awaited test completes, including when it raises an +exception. + + Pytest usage ~~~~~~~~~~~~ diff --git a/tests/test_test_overrides.py b/tests/test_test_overrides.py index 53fc6d13..926adf29 100644 --- a/tests/test_test_overrides.py +++ b/tests/test_test_overrides.py @@ -1,4 +1,10 @@ +import asyncio +from unittest.mock import patch + +from asgiref.sync import async_to_sync +from asgiref.sync import iscoroutinefunction from django.test import TestCase +from django.test import TransactionTestCase from constance import config from constance.test import override_config @@ -35,3 +41,51 @@ class OverrideConfigClassDecoratorTestCase(TestCase): def test_override_config_on_class_changes_config_value(self): """Assert that the class decorator changes config.BOOL_VALUE.""" self.assertFalse(config.BOOL_VALUE) + + +class OverrideConfigAsyncDecoratorTestCase(TransactionTestCase): + @override_config(BOOL_VALUE=False) + async def test_override_config_on_async_method(self): + self.assertFalse(await config.BOOL_VALUE) + await asyncio.sleep(0) + self.assertFalse(await config.BOOL_VALUE) + + def test_async_override_lasts_until_await_completes(self): + @override_config(BOOL_VALUE=False) + async def decorated(): + self.assertFalse(await config.BOOL_VALUE) + await asyncio.sleep(0) + self.assertFalse(await config.BOOL_VALUE) + return "completed" + + self.assertTrue(iscoroutinefunction(decorated)) + self.assertEqual(async_to_sync(decorated)(), "completed") + self.assertTrue(config.BOOL_VALUE) + + def test_async_override_restores_after_exception(self): + @override_config(BOOL_VALUE=False) + async def decorated(): + self.assertFalse(await config.BOOL_VALUE) + await asyncio.sleep(0) + raise ValueError("test failure") + + with self.assertRaisesMessage(ValueError, "test failure"): + async_to_sync(decorated)() + self.assertTrue(config.BOOL_VALUE) + + def test_async_override_with_database_backend(self): + from constance import settings + from constance.base import Config + + with patch.object(settings, "BACKEND", "constance.backends.database.DatabaseBackend"): + database_config = Config() + with patch("constance.test.unittest.config", database_config): + + @override_config(BOOL_VALUE=False) + async def decorated(): + self.assertFalse(await database_config.BOOL_VALUE) + await asyncio.sleep(0) + self.assertFalse(await database_config.BOOL_VALUE) + + async_to_sync(decorated)() + self.assertTrue(database_config.BOOL_VALUE) From 032753eb065cc4aa2fdddf53ba3a5e47f5fd668d Mon Sep 17 00:00:00 2001 From: Alexandr Artemyev Date: Wed, 28 Oct 2026 11:35:43 +0500 Subject: [PATCH 2/2] Fix indentation and update sync_to_async usage Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- constance/test/unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constance/test/unittest.py b/constance/test/unittest.py index 2b7977b7..b023c840 100644 --- a/constance/test/unittest.py +++ b/constance/test/unittest.py @@ -33,8 +33,8 @@ def __call__(self, test_func): @wraps(test_func) async def inner(*args, **kwargs): - await sync_to_async(self.enable, thread_sensitive=True)() try: + await sync_to_async(self.enable, thread_sensitive=True)() return await test_func(*args, **kwargs) finally: await sync_to_async(self.disable, thread_sensitive=True)()