Coverage for lazy_settings/test/utils.py: 85%
126 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-11 03:57 +0330
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-11 03:57 +0330
1from functools import wraps
2from inspect import iscoroutinefunction
3from unittest import TestCase
5from lazy_settings.conf import UserSettingsHolder, settings
7try:
8 import pytest
9except ImportError:
10 pass
13class TestContextDecorator:
14 """
15 A base class that can either be used as a context manager during tests
16 or as a test function or unittest.TestCase subclass decorator to perform
17 temporary alterations.
19 `attr_name`: attribute assigned the return value of enable() if used as
20 a class decorator.
22 `kwarg_name`: keyword argument passing the return value of enable() if
23 used as a function decorator.
24 """
26 def __init__(self, attr_name=None, kwarg_name=None):
27 self.attr_name = attr_name
28 self.kwarg_name = kwarg_name
30 def enable(self):
31 raise NotImplementedError
33 def disable(self):
34 raise NotImplementedError
36 def __enter__(self):
37 return self.enable()
39 def __exit__(self, exc_type, exc_value, traceback):
40 self.disable()
42 def decorate_class(self, cls):
43 if issubclass(cls, TestCase):
44 decorated_setUp = cls.setUp
46 def setUp(inner_self):
47 context = self.enable()
48 inner_self.addCleanup(self.disable)
49 if self.attr_name:
50 setattr(inner_self, self.attr_name, context)
51 decorated_setUp(inner_self)
53 cls.setUp = setUp
55 else:
57 @pytest.fixture(autouse=True)
58 def settings_fixture(inner_self):
59 context = self.enable()
60 if self.attr_name:
61 setattr(inner_self, self.attr_name, context)
63 yield context
64 self.disable()
66 cls.settings_fixture = settings_fixture
68 return cls
70 def decorate_callable(self, func):
71 if iscoroutinefunction(func):
72 # If the inner function is an async function, we must execute async
73 # as well so that the `with` statement executes at the right time.
74 @wraps(func)
75 async def inner(*args, **kwargs):
76 with self as context:
77 if self.kwarg_name:
78 kwargs[self.kwarg_name] = context
79 return await func(*args, **kwargs)
81 else:
83 @wraps(func)
84 def inner(*args, **kwargs):
85 with self as context:
86 if self.kwarg_name:
87 kwargs[self.kwarg_name] = context
88 return func(*args, **kwargs)
90 return inner
92 def __call__(self, decorated):
93 if isinstance(decorated, type):
94 return self.decorate_class(decorated)
95 elif callable(decorated):
96 return self.decorate_callable(decorated)
97 raise TypeError("Cannot decorate object of type %s" % type(decorated))
100class override_settings(TestContextDecorator):
101 """
102 Act as either a decorator or a context manager. If it's a decorator, take a
103 function and return a wrapped function. If it's a contextmanager, use it
104 with the ``with`` statement. In either event, entering/exiting are called
105 before and after, respectively, the function/block is executed.
106 """
108 enable_exception = None
110 def __init__(self, **kwargs):
111 self.options = kwargs
112 super().__init__()
114 def enable(self):
115 override = UserSettingsHolder(settings._wrapped)
116 for key, new_value in self.options.items():
117 setattr(override, key, new_value)
119 self.wrapped = settings._wrapped
120 settings._wrapped = override
122 def disable(self):
123 settings._wrapped = self.wrapped
124 del self.wrapped
125 responses = []
127 if self.enable_exception is not None:
128 exc = self.enable_exception
129 self.enable_exception = None
130 raise exc
131 for _, response in responses:
132 if isinstance(response, Exception):
133 raise response
135 def django_save_options(self, test_func):
136 if test_func._overridden_settings is None:
137 test_func._overridden_settings = self.options
138 else:
139 # Duplicate dict to prevent subclasses from altering their parent.
140 test_func._overridden_settings = {
141 **test_func._overridden_settings,
142 **self.options,
143 }
145 def decorate_class(self, cls):
146 try:
147 from django.test import SimpleTestCase
149 if issubclass(cls, SimpleTestCase):
150 self.django_save_options(cls)
151 return cls
152 except ImportError:
153 pass
155 return super().decorate_class(cls)
158class modify_settings(override_settings):
159 """
160 Like override_settings, but makes it possible to append, prepend, or remove
161 items instead of redefining the entire list.
162 """
164 def __init__(self, *args, **kwargs):
165 if args:
166 # Hack used when instantiating from SimpleTestCase.setUpClass.
167 assert not kwargs
168 self.operations = args[0]
169 else:
170 assert not args
171 self.operations = list(kwargs.items())
172 super().__init__()
174 def django_save_options(self, test_func):
175 if test_func._modified_settings is None:
176 test_func._modified_settings = self.operations
177 else:
178 # Duplicate list to prevent subclasses from altering their parent.
179 test_func._modified_settings = (
180 list(test_func._modified_settings) + self.operations
181 )
183 def enable(self):
184 self.options = {}
185 for name, operations in self.operations:
186 try:
187 # When called from SimpleTestCase.setUpClass, values may be
188 # overridden several times; cumulate changes.
189 value = self.options[name]
190 except KeyError:
191 value = list(getattr(settings, name, []))
192 for action, items in operations.items():
193 # items may be a single value or an iterable.
194 if isinstance(items, str):
195 items = [items]
196 if action == "append":
197 value += [item for item in items if item not in value]
198 elif action == "prepend":
199 value = [item for item in items if item not in value] + value
200 elif action == "remove":
201 value = [item for item in value if item not in items]
202 else:
203 raise ValueError("Unsupported action: %s" % action)
204 self.options[name] = value
205 super().enable()