Skip to content

Commit 27a6aff

Browse files
add ability to autoconfigure application in template settings (#5)
1 parent d826b6b commit 27a6aff

File tree

3 files changed

+300
-4
lines changed

3 files changed

+300
-4
lines changed

src/django_bird/apps.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import annotations
2+
3+
from django.apps import AppConfig
4+
5+
from ._typing import override
6+
7+
8+
class DjangoBirdAppConfig(AppConfig):
9+
label = "django_bird"
10+
name = "django_bird"
11+
verbose_name = "Bird"
12+
13+
@override
14+
def ready(self):
15+
from django_bird.conf import app_settings
16+
17+
app_settings.autoconfigure()

src/django_bird/conf.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,90 @@
11
from __future__ import annotations
22

3+
from contextlib import suppress
34
from dataclasses import dataclass
5+
from dataclasses import field
6+
from typing import Any
47

8+
import django.template
59
from django.conf import settings
610

711
from ._typing import override
812

913
DJANGO_BIRD_SETTINGS_NAME = "DJANGO_BIRD"
1014

1115

12-
@dataclass(frozen=True)
16+
@dataclass
1317
class AppSettings:
18+
ENABLE_AUTO_CONFIG: bool = True
19+
_template_configurator: TemplateConfigurator = field(init=False)
20+
21+
def __post_init__(self):
22+
self._template_configurator = TemplateConfigurator(self)
23+
1424
@override
1525
def __getattribute__(self, __name: str) -> object:
1626
user_settings = getattr(settings, DJANGO_BIRD_SETTINGS_NAME, {})
1727
return user_settings.get(__name, super().__getattribute__(__name)) # pyright: ignore[reportAny]
1828

29+
def autoconfigure(self) -> None:
30+
if not self.ENABLE_AUTO_CONFIG:
31+
return
32+
33+
self._template_configurator.autoconfigure()
34+
35+
36+
class TemplateConfigurator:
37+
def __init__(self, app_settings: AppSettings, engine_name: str = "django") -> None:
38+
self.app_settings = app_settings
39+
self.engine_name = engine_name
40+
self._configured = False
41+
42+
def autoconfigure(self) -> None:
43+
for template_config in settings.TEMPLATES:
44+
engine_name = (
45+
template_config.get("NAME") or template_config["BACKEND"].split(".")[-2]
46+
)
47+
if engine_name != self.engine_name:
48+
return
49+
50+
options = template_config.setdefault("OPTIONS", {})
51+
52+
self.configure_loaders(options)
53+
self.configure_builtins(options)
54+
55+
# Force re-evaluation of settings.TEMPLATES because EngineHandler caches it.
56+
with suppress(AttributeError):
57+
del django.template.engines.templates
58+
django.template.engines._engines = {} # type: ignore[attr-defined]
59+
60+
self._configured = True
61+
62+
def configure_loaders(self, options: dict[str, Any]) -> None:
63+
loaders = options.setdefault("loaders", [])
64+
65+
# find the inner-most loaders, which is an iterable of only strings
66+
while not all(isinstance(loader, str) for loader in loaders):
67+
for loader in loaders:
68+
# if we've found a list or tuple, we aren't yet in the inner-most loaders
69+
if isinstance(loader, list | tuple):
70+
# reassign `loaders` variable to force the while loop restart
71+
loaders = loader
72+
73+
# if django-bird's loader is the first, we good
74+
loaders_already_configured = (
75+
len(loaders) > 0 and "django_bird.loader.BirdLoader" == loaders[0]
76+
)
77+
78+
if not loaders_already_configured:
79+
loaders.insert(0, "django_bird.loader.BirdLoader")
80+
81+
def configure_builtins(self, options: dict[str, Any]) -> None:
82+
builtins = options.setdefault("builtins", [])
83+
84+
builtins_already_configured = "django_bird.templatetags.django_bird" in builtins
85+
86+
if not builtins_already_configured:
87+
builtins.append("django_bird.templatetags.django_bird")
88+
1989

2090
app_settings = AppSettings()

tests/test_conf.py

Lines changed: 212 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,220 @@
11
from __future__ import annotations
22

33
import pytest
4+
from django.conf import settings
5+
from django.test import override_settings
46

7+
from django_bird.conf import AppSettings
8+
from django_bird.conf import TemplateConfigurator
59
from django_bird.conf import app_settings
610

711

812
def test_app_settings():
9-
# stub test until `bird` requires custom app settings
10-
with pytest.raises(AttributeError):
11-
assert app_settings.foo
13+
assert app_settings.ENABLE_AUTO_CONFIG is True
14+
15+
16+
@override_settings(
17+
DJANGO_BIRD={
18+
"ENABLE_AUTO_CONFIG": False,
19+
}
20+
)
21+
def test_autoconfigure_disabled():
22+
app_settings = AppSettings()
23+
template_options = settings.TEMPLATES[0]["OPTIONS"]
24+
25+
assert app_settings.ENABLE_AUTO_CONFIG is False
26+
assert "django_bird.templatetags.django_bird" not in template_options["loaders"]
27+
assert "django_bird.loader.BirdLoader" not in template_options["builtins"]
28+
29+
30+
class TestTemplateConfigurator:
31+
DJANGO_BIRD_BUILTINS = "django_bird.templatetags.django_bird"
32+
DJANGO_BIRD_LOADER = "django_bird.loader.BirdLoader"
33+
34+
@pytest.fixture
35+
def configurator(self):
36+
return TemplateConfigurator(app_settings)
37+
38+
@pytest.fixture(autouse=True)
39+
def reset_settings(self):
40+
template_options = settings.TEMPLATES[0]["OPTIONS"]
41+
42+
assert self.DJANGO_BIRD_LOADER in template_options["loaders"]
43+
assert self.DJANGO_BIRD_BUILTINS in template_options["builtins"]
44+
45+
with override_settings(
46+
TEMPLATES=[
47+
settings.TEMPLATES[0]
48+
| {
49+
**settings.TEMPLATES[0],
50+
"OPTIONS": {
51+
"loaders": [
52+
loader
53+
for loader in template_options["loaders"]
54+
if loader != self.DJANGO_BIRD_LOADER
55+
],
56+
"builtins": [
57+
builtin
58+
for builtin in template_options["builtins"]
59+
if builtin != self.DJANGO_BIRD_BUILTINS
60+
],
61+
},
62+
}
63+
]
64+
):
65+
options = settings.TEMPLATES[0]["OPTIONS"]
66+
67+
assert self.DJANGO_BIRD_LOADER not in options["loaders"]
68+
assert self.DJANGO_BIRD_BUILTINS not in options["builtins"]
69+
70+
yield
71+
72+
def test_autoconfigure(self, configurator):
73+
template_options = settings.TEMPLATES[0]["OPTIONS"]
74+
75+
configurator.autoconfigure()
76+
77+
assert self.DJANGO_BIRD_LOADER in template_options["loaders"]
78+
assert self.DJANGO_BIRD_BUILTINS in template_options["builtins"]
79+
80+
def test_configure_loaders(self, configurator):
81+
template_options = settings.TEMPLATES[0]["OPTIONS"]
82+
83+
configurator.configure_loaders(template_options)
84+
85+
assert self.DJANGO_BIRD_LOADER in template_options["loaders"]
86+
87+
def test_configure_builtins(self, configurator):
88+
template_options = settings.TEMPLATES[0]["OPTIONS"]
89+
90+
configurator.configure_builtins(template_options)
91+
92+
assert self.DJANGO_BIRD_BUILTINS in template_options["builtins"]
93+
94+
def test_configured(self, configurator):
95+
assert configurator._configured is False
96+
97+
configurator.autoconfigure()
98+
99+
assert configurator._configured is True
100+
101+
@pytest.mark.parametrize(
102+
"init_options,expected",
103+
[
104+
(
105+
{
106+
"builtins": [
107+
"django.template.defaulttags",
108+
],
109+
"loaders": [
110+
"django.template.loaders.filesystem.Loader",
111+
"django.template.loaders.app_directories.Loader",
112+
],
113+
},
114+
{
115+
"builtins": [
116+
"django.template.defaulttags",
117+
"django_bird.templatetags.django_bird",
118+
],
119+
"loaders": [
120+
"django_bird.loader.BirdLoader",
121+
"django.template.loaders.filesystem.Loader",
122+
"django.template.loaders.app_directories.Loader",
123+
],
124+
},
125+
),
126+
(
127+
{
128+
"builtins": [
129+
"django.template.defaulttags",
130+
],
131+
"loaders": [
132+
(
133+
"django.template.loaders.cached.Loader",
134+
[
135+
"django.template.loaders.filesystem.Loader",
136+
"django.template.loaders.app_directories.Loader",
137+
],
138+
),
139+
],
140+
},
141+
{
142+
"builtins": [
143+
"django.template.defaulttags",
144+
"django_bird.templatetags.django_bird",
145+
],
146+
"loaders": [
147+
(
148+
"django.template.loaders.cached.Loader",
149+
[
150+
"django_bird.loader.BirdLoader",
151+
"django.template.loaders.filesystem.Loader",
152+
"django.template.loaders.app_directories.Loader",
153+
],
154+
),
155+
],
156+
},
157+
),
158+
(
159+
{
160+
"builtins": [
161+
"django.template.defaulttags",
162+
],
163+
"loaders": [
164+
(
165+
"template_partials.loader.Loader",
166+
[
167+
(
168+
"django.template.loaders.cached.Loader",
169+
[
170+
"django.template.loaders.filesystem.Loader",
171+
"django.template.loaders.app_directories.Loader",
172+
],
173+
),
174+
],
175+
)
176+
],
177+
},
178+
{
179+
"builtins": [
180+
"django.template.defaulttags",
181+
"django_bird.templatetags.django_bird",
182+
],
183+
"loaders": [
184+
(
185+
"template_partials.loader.Loader",
186+
[
187+
(
188+
"django.template.loaders.cached.Loader",
189+
[
190+
"django_bird.loader.BirdLoader",
191+
"django.template.loaders.filesystem.Loader",
192+
"django.template.loaders.app_directories.Loader",
193+
],
194+
),
195+
],
196+
)
197+
],
198+
},
199+
),
200+
],
201+
)
202+
def test_template_settings(self, init_options, expected, configurator):
203+
with override_settings(
204+
TEMPLATES=[
205+
settings.TEMPLATES[0]
206+
| {
207+
**settings.TEMPLATES[0],
208+
"OPTIONS": init_options,
209+
}
210+
]
211+
):
212+
template_options = settings.TEMPLATES[0]["OPTIONS"]
213+
214+
assert template_options == init_options
215+
assert self.DJANGO_BIRD_LOADER not in template_options["loaders"]
216+
assert self.DJANGO_BIRD_BUILTINS not in template_options["builtins"]
217+
218+
configurator.autoconfigure()
219+
220+
assert template_options == expected

0 commit comments

Comments
 (0)