Skip to content

Commit e90cdba

Browse files
authored
INSTALLED_APPS duplicate-registration guard (#153)
1 parent 637e357 commit e90cdba

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

src/boost_weblate/settings_override.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def merge_boost_endpoint_throttle_rates(
222222
globals()["REST_FRAMEWORK"] = merge_boost_endpoint_throttle_rates(_REST_FRAMEWORK)
223223

224224
_INSTALLED_APPS = globals().get("INSTALLED_APPS")
225-
if _INSTALLED_APPS is not None:
225+
if _INSTALLED_APPS is not None and _ENDPOINT_APP_CONFIG not in _INSTALLED_APPS:
226226
# Tuple += creates a new object; assign back so exec namespace / settings see it.
227227
# List += mutates in place, matching Weblate/Docker settings namespaces.
228228
if isinstance(_INSTALLED_APPS, tuple):

tests/test_settings_override.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,31 @@
88

99
import importlib
1010
import importlib.util
11+
import subprocess
12+
import sys
13+
import textwrap
14+
from collections.abc import Callable
1115
from pathlib import Path
1216

1317
import pytest
1418

1519
from boost_weblate.formats import registry
1620

21+
_ENDPOINT_APP_CONFIG = "boost_weblate.endpoint.apps.BoostEndpointConfig"
22+
_REPO_ROOT = Path(__file__).resolve().parents[1]
23+
_SETTINGS_OVERRIDE_PATH = _REPO_ROOT / "src/boost_weblate/settings_override.py"
24+
25+
26+
def _exec_settings_override(namespace: dict) -> None:
27+
exec(
28+
compile(
29+
_SETTINGS_OVERRIDE_PATH.read_text(encoding="utf-8"),
30+
str(_SETTINGS_OVERRIDE_PATH),
31+
"exec",
32+
),
33+
namespace,
34+
)
35+
1736

1837
def _plugin_weblate_paths() -> tuple[str, ...]:
1938
return registry.weblate_class_paths()
@@ -143,3 +162,120 @@ def test_boost_task_timeout_settings_rejects_invalid_limits(
143162
monkeypatch.setenv("BOOST_TASK_TIME_LIMIT", "900")
144163
with pytest.raises(ValueError, match="BOOST_TASK_TIME_LIMIT"):
145164
boost_task_timeout_settings()
165+
166+
167+
@pytest.mark.parametrize(
168+
"factory",
169+
[
170+
lambda: ["django.contrib.auth"],
171+
lambda: ("django.contrib.auth",),
172+
],
173+
ids=["list", "tuple"],
174+
)
175+
def test_double_exec_does_not_duplicate_installed_apps(
176+
factory: Callable[[], list[str] | tuple[str, ...]],
177+
) -> None:
178+
base_apps = factory()
179+
ns: dict[str, object] = {"INSTALLED_APPS": base_apps}
180+
_exec_settings_override(ns)
181+
_exec_settings_override(ns)
182+
apps = ns["INSTALLED_APPS"]
183+
assert apps.count(_ENDPOINT_APP_CONFIG) == 1
184+
assert apps[0] == "django.contrib.auth"
185+
if isinstance(base_apps, tuple):
186+
assert isinstance(apps, tuple)
187+
188+
189+
def test_double_exec_does_not_double_ready_hooks() -> None:
190+
script = textwrap.dedent(
191+
f"""
192+
import os
193+
import sys
194+
import tempfile
195+
import types
196+
from pathlib import Path
197+
198+
repo = Path({str(_REPO_ROOT)!r})
199+
sys.path.insert(0, str(repo))
200+
sys.path.insert(0, str(repo / "src"))
201+
202+
import weblate.settings_example as _wl_example
203+
204+
ns: dict[str, object] = {{}}
205+
for _key, _value in _wl_example.__dict__.items():
206+
if _key.isupper():
207+
ns[_key] = _value
208+
209+
ns["INSTALLED_APPS"] = tuple(
210+
app
211+
for app in _wl_example.INSTALLED_APPS
212+
if app != "django.contrib.postgres"
213+
)
214+
215+
_data = tempfile.mkdtemp(prefix="double_exec_settings_")
216+
ns["DATA_DIR"] = _data
217+
ns["CACHE_DIR"] = os.path.join(_data, "cache")
218+
ns["MEDIA_ROOT"] = os.path.join(_data, "media")
219+
ns["STATIC_ROOT"] = os.path.join(_data, "static")
220+
for _p in (ns["CACHE_DIR"], ns["MEDIA_ROOT"], ns["STATIC_ROOT"]):
221+
os.makedirs(_p, exist_ok=True)
222+
223+
ns["DATABASES"] = {{
224+
"default": {{
225+
"ENGINE": "django.db.backends.sqlite3",
226+
"NAME": os.path.join(_data, "test.sqlite3"),
227+
}}
228+
}}
229+
ns["SITE_DOMAIN"] = "test.invalid"
230+
ns["DEBUG"] = False
231+
ns["CELERY_TASK_ALWAYS_EAGER"] = True
232+
ns["CELERY_BROKER_URL"] = "memory://"
233+
ns["CELERY_TASK_EAGER_PROPAGATES"] = True
234+
ns["CELERY_RESULT_BACKEND"] = None
235+
ns["CACHES"] = {{
236+
"default": {{"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
237+
}}
238+
ns["PASSWORD_HASHERS"] = ["django.contrib.auth.hashers.MD5PasswordHasher"]
239+
240+
override_path = Path({str(_SETTINGS_OVERRIDE_PATH)!r})
241+
override_code = compile(
242+
override_path.read_text(encoding="utf-8"),
243+
str(override_path),
244+
"exec",
245+
)
246+
exec(override_code, ns)
247+
exec(override_code, ns)
248+
249+
settings_mod = types.ModuleType("tests._double_exec_settings")
250+
for _key, _value in ns.items():
251+
if _key.isupper():
252+
setattr(settings_mod, _key, _value)
253+
sys.modules["tests._double_exec_settings"] = settings_mod
254+
os.environ["DJANGO_SETTINGS_MODULE"] = "tests._double_exec_settings"
255+
256+
from boost_weblate.endpoint.apps import BoostEndpointConfig
257+
258+
ready_calls: list[int] = []
259+
_original_ready = BoostEndpointConfig.ready
260+
261+
def _counting_ready(self: BoostEndpointConfig) -> None:
262+
ready_calls.append(1)
263+
return _original_ready(self)
264+
265+
BoostEndpointConfig.ready = _counting_ready # type: ignore[method-assign]
266+
267+
import django
268+
269+
django.setup()
270+
print(f"ready_calls={{len(ready_calls)}}")
271+
"""
272+
)
273+
result = subprocess.run(
274+
[sys.executable, "-c", script],
275+
capture_output=True,
276+
text=True,
277+
cwd=_REPO_ROOT,
278+
check=False,
279+
)
280+
assert result.returncode == 0, result.stderr
281+
assert "ready_calls=1" in result.stdout

0 commit comments

Comments
 (0)