Skip to content

Commit b9ae2ed

Browse files
simplify asset registry by moving loader attr to global registry (#79)
1 parent b4f8970 commit b9ae2ed

File tree

8 files changed

+176
-99
lines changed

8 files changed

+176
-99
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2222

2323
- Improved handling of quoted vs unquoted attribute values in `{% bird %}` components. Quoted values (e.g., `class="static-class"`) are treated as literal strings, while unquoted values (e.g., `class=dynamic_class`) are resolved from the template context. This allows for more explicit control over whether an attribute value should be treated as a literal or resolved dynamically.
2424

25+
### Fixed
26+
27+
- **Internal**: Simplified asset management by using a global registry, making it work reliably with any template loader configuration.
28+
2529
## [0.6.2]
2630

2731
### Changed

src/django_bird/loader.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ._typing import override
1818
from .compiler import Compiler
1919
from .components import components
20-
from .staticfiles import ComponentAssetRegistry
20+
from .staticfiles import assets
2121
from .templatetags.tags.bird import TAG
2222
from .templatetags.tags.bird import BirdNode
2323

@@ -28,7 +28,6 @@ class BirdLoader(FileSystemLoader):
2828
def __init__(self, engine: Engine):
2929
super().__init__(engine)
3030
self.compiler = Compiler()
31-
self.asset_registry = ComponentAssetRegistry()
3231

3332
@override
3433
def get_contents(self, origin: Origin) -> str:
@@ -52,15 +51,15 @@ def get_contents(self, origin: Origin) -> str:
5251
def _scan_for_components(self, node: Template | Node, context: Context) -> None:
5352
if isinstance(node, BirdNode):
5453
component = components.get_component(node.name)
55-
self.asset_registry.register(component)
54+
assets.register(component)
5655

5756
if not _has_nodelist(node) or node.nodelist is None:
5857
return
5958

6059
for child in node.nodelist:
6160
if isinstance(child, BirdNode):
6261
component = components.get_component(child.name)
63-
self.asset_registry.register(component)
62+
assets.register(component)
6463

6564
if isinstance(child, ExtendsNode):
6665
parent_template = child.get_parent(context)

src/django_bird/staticfiles.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,18 @@ def get_template_assets(template: DjangoTemplate):
6262
class ComponentAssetRegistry:
6363
components: set[Component] = field(default_factory=set)
6464

65+
def clear(self) -> None:
66+
"""Clear the component cache. Mainly useful for testing."""
67+
self.components.clear()
68+
6569
def register(self, component: Component) -> None:
6670
self.components.add(component)
6771

68-
def get_assets(self, asset_type: AssetType) -> set[Asset]:
69-
assets: set[Asset] = set()
72+
def get_assets(self, asset_type: AssetType) -> list[Asset]:
73+
assets: list[Asset] = []
7074
for component in self.components:
71-
assets.update(a for a in component.assets if a.type == asset_type)
75+
assets.extend(a for a in component.assets if a.type == asset_type)
7276
return assets
77+
78+
79+
assets = ComponentAssetRegistry()

src/django_bird/templatetags/tags/asset.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
from django_bird._typing import TagBits
1010
from django_bird._typing import override
11-
from django_bird.loader import BirdLoader
1211
from django_bird.staticfiles import Asset
1312
from django_bird.staticfiles import AssetType
13+
from django_bird.staticfiles import assets
1414

1515
CSS_TAG = "bird:css"
1616
JS_TAG = "bird:js"
@@ -48,18 +48,10 @@ def __init__(self, asset_type: AssetType):
4848

4949
@override
5050
def render(self, context: Context) -> str:
51-
template = context.template
52-
if template is None:
53-
return ""
54-
loaders = template.engine.template_loaders
55-
for loader in loaders:
56-
if isinstance(loader, BirdLoader):
57-
assets = loader.asset_registry.get_assets(self.asset_type)
58-
return self._render_assets(assets)
59-
60-
raise RuntimeError("BirdLoader not found in template loaders")
51+
component_assets = assets.get_assets(self.asset_type)
52+
return self._render_assets(component_assets)
6153

62-
def _render_assets(self, assets: set[Asset]) -> str:
54+
def _render_assets(self, assets: list[Asset]) -> str:
6355
if not assets:
6456
return ""
6557

tests/conftest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,18 @@ def func(text):
164164

165165

166166
@pytest.fixture(autouse=True)
167-
def clear_registry():
167+
def clear_components_registry():
168168
from django_bird.components import components
169169

170170
components.clear()
171171
yield
172172
components.clear()
173+
174+
175+
@pytest.fixture(autouse=True)
176+
def clear_assets_registry():
177+
from django_bird.staticfiles import assets
178+
179+
assets.clear()
180+
yield
181+
assets.clear()

tests/templatetags/test_asset.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.template.base import Token
66
from django.template.base import TokenType
77
from django.template.context import Context
8-
from django.template.engine import Engine
98
from django.template.exceptions import TemplateSyntaxError
109

1110
from django_bird.staticfiles import AssetType
@@ -155,29 +154,6 @@ def test_component_render_order(
155154
assert f'<script src="{first_js}"></script>' in rendered[body_start:]
156155
assert f'<script src="{second_js}"></script>' in rendered[body_start:]
157156

158-
def test_birdloader_required(self, templates_dir, create_template):
159-
template_path = templates_dir / "test.html"
160-
template_path.write_text("""
161-
<html>
162-
<head>
163-
{% bird:css %}
164-
</head>
165-
<body>
166-
{% bird:js %}
167-
</body>
168-
</html>
169-
""")
170-
171-
engine = Engine(
172-
builtins=["django_bird.templatetags.django_bird"],
173-
dirs=[str(templates_dir)],
174-
loaders=["django.template.loaders.filesystem.Loader"],
175-
)
176-
template = engine.get_template("test.html")
177-
178-
with pytest.raises(RuntimeError):
179-
template.render(Context({}))
180-
181157

182158
class TestNode:
183159
def test_no_template(self):

tests/test_loader.py

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django_bird.loader import BIRD_TAG_PATTERN
1212
from django_bird.loader import BirdLoader
1313
from django_bird.params import Params
14+
from django_bird.staticfiles import assets
1415
from django_bird.templatetags.tags.bird import BirdNode
1516

1617

@@ -55,60 +56,6 @@ def test_render_template(template_name):
5556
assert rendered
5657

5758

58-
def test_asset_registry(
59-
create_bird_template, create_bird_asset, create_template, templates_dir
60-
):
61-
alert = create_bird_template("alert", '<div class="alert">{{ slot }}</div>')
62-
create_bird_asset(alert, ".alert { color: red; }", "css")
63-
create_bird_asset(alert, "console.log('alert');", "js")
64-
65-
badge = create_bird_template("badge", "<span>{{ slot }}</span>")
66-
create_bird_asset(badge, ".badge { color: blue; }", "css")
67-
create_bird_asset(badge, "console.log('badge');", "js")
68-
69-
button = create_bird_template("button", "<button>{{ slot }}</button>")
70-
create_bird_asset(button, ".button { color: blue; }", "css")
71-
create_bird_asset(button, "console.log('button');", "js")
72-
73-
base_path = templates_dir / "base.html"
74-
base_path.write_text("""
75-
<html>
76-
<head>
77-
<title>Test</title>
78-
{% bird:css %}
79-
</head>
80-
<body>
81-
{% bird alert %}Base Alert{% endbird %}
82-
{% block content %}{% endblock %}
83-
{% bird:js %}
84-
</body>
85-
</html>
86-
""")
87-
88-
include_path = templates_dir / "include.html"
89-
include_path.write_text("""
90-
{% bird badge %}Active{% endbird %}
91-
""")
92-
93-
child_path = templates_dir / "child.html"
94-
child_path.write_text("""
95-
{% extends 'base.html' %}
96-
{% block content %}
97-
{% bird button %}Click me{% endbird %}
98-
{% include 'include.html' %}
99-
{% endblock %}
100-
""")
101-
102-
template = create_template(child_path)
103-
104-
engine = template.template.engine
105-
loader = engine.template_loaders[0]
106-
107-
components = loader.asset_registry.components
108-
109-
assert len(components) == 3
110-
111-
11259
@pytest.mark.parametrize(
11360
"node,expected_count",
11461
[
@@ -140,4 +87,4 @@ def test_scan_for_components(
14087

14188
loader._scan_for_components(node, context)
14289

143-
assert len(loader.asset_registry.components) == expected_count
90+
assert len(assets.components) == expected_count

tests/test_staticfiles.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
import pytest
66

7+
from django_bird.components import components
78
from django_bird.staticfiles import Asset
89
from django_bird.staticfiles import AssetType
10+
from django_bird.staticfiles import assets
911
from django_bird.staticfiles import get_template_assets
1012

1113

@@ -119,3 +121,144 @@ def test_custom_component_dir(self, create_template, create_bird_template):
119121

120122
assert len(assets) == 1
121123
assert Asset(css_file, AssetType.CSS) in assets
124+
125+
126+
class TestComponentAssetRegistry:
127+
def test_asset_registry(
128+
self, create_bird_template, create_bird_asset, create_template, templates_dir
129+
):
130+
alert = create_bird_template("alert", '<div class="alert">{{ slot }}</div>')
131+
create_bird_asset(alert, ".alert { color: red; }", "css")
132+
create_bird_asset(alert, "console.log('alert');", "js")
133+
134+
badge = create_bird_template("badge", "<span>{{ slot }}</span>")
135+
create_bird_asset(badge, ".badge { color: blue; }", "css")
136+
create_bird_asset(badge, "console.log('badge');", "js")
137+
138+
button = create_bird_template("button", "<button>{{ slot }}</button>")
139+
create_bird_asset(button, ".button { color: blue; }", "css")
140+
create_bird_asset(button, "console.log('button');", "js")
141+
142+
base_path = templates_dir / "base.html"
143+
base_path.write_text("""
144+
<html>
145+
<head>
146+
<title>Test</title>
147+
{% bird:css %}
148+
</head>
149+
<body>
150+
{% bird alert %}Base Alert{% endbird %}
151+
{% block content %}{% endblock %}
152+
{% bird:js %}
153+
</body>
154+
</html>
155+
""")
156+
157+
include_path = templates_dir / "include.html"
158+
include_path.write_text("""
159+
{% bird badge %}Active{% endbird %}
160+
""")
161+
162+
child_path = templates_dir / "child.html"
163+
child_path.write_text("""
164+
{% extends 'base.html' %}
165+
{% block content %}
166+
{% bird button %}Click me{% endbird %}
167+
{% include 'include.html' %}
168+
{% endblock %}
169+
""")
170+
171+
create_template(child_path)
172+
173+
assert len(assets.components) == 3
174+
175+
def test_get_assets_by_type(
176+
self, create_bird_template, create_bird_asset, create_template
177+
):
178+
template_file = create_bird_template("test", "<div>Test</div>")
179+
css_asset = create_bird_asset(template_file, ".test { color: red; }", "css")
180+
js_asset = create_bird_asset(template_file, "console.log('test');", "js")
181+
182+
component = components.get_component("test")
183+
assets.register(component)
184+
185+
css_assets = assets.get_assets(AssetType.CSS)
186+
js_assets = assets.get_assets(AssetType.JS)
187+
188+
assert len(css_assets) == 1
189+
assert len(js_assets) == 1
190+
assert Asset(Path(css_asset), AssetType.CSS) in css_assets
191+
assert Asset(Path(js_asset), AssetType.JS) in js_assets
192+
193+
def test_register_same_component_multiple_times(
194+
self, create_bird_template, create_bird_asset
195+
):
196+
template_file = create_bird_template("test", "<div>Test</div>")
197+
create_bird_asset(template_file, ".test { color: red; }", "css")
198+
199+
component = components.get_component("test")
200+
201+
assets.register(component)
202+
assets.register(component)
203+
204+
assert len(assets.components) == 1
205+
assert len(assets.get_assets(AssetType.CSS)) == 1
206+
207+
def test_multiple_components_same_asset_names(
208+
self, create_bird_template, create_bird_asset
209+
):
210+
template1 = create_bird_template("comp1", "<div>One</div>", sub_dir="first")
211+
template2 = create_bird_template("comp2", "<div>Two</div>", sub_dir="second")
212+
213+
css1 = create_bird_asset(template1, ".one { color: red; }", "css")
214+
css2 = create_bird_asset(template2, ".two { color: blue; }", "css")
215+
216+
comp1 = components.get_component("first/comp1")
217+
comp2 = components.get_component("second/comp2")
218+
219+
assets.register(comp1)
220+
assets.register(comp2)
221+
222+
css_assets = assets.get_assets(AssetType.CSS)
223+
assert len(css_assets) == 2
224+
225+
asset_paths = {str(asset.path) for asset in css_assets}
226+
assert str(css1) in asset_paths
227+
assert str(css2) in asset_paths
228+
229+
def test_template_inheritance_assets(
230+
self, create_bird_template, create_bird_asset, create_template, templates_dir
231+
):
232+
parent = create_bird_template("parent", "<div>Parent</div>")
233+
child = create_bird_template("child", "<div>Child</div>")
234+
235+
parent_css = create_bird_asset(parent, ".parent { color: red; }", "css")
236+
child_css = create_bird_asset(child, ".child { color: blue; }", "css")
237+
238+
base_path = templates_dir / "base.html"
239+
base_path.write_text("""
240+
{% bird parent %}Parent Content{% endbird %}
241+
{% block content %}{% endblock %}
242+
""")
243+
244+
child_path = templates_dir / "child.html"
245+
child_path.write_text("""
246+
{% extends 'base.html' %}
247+
{% block content %}
248+
{% bird child %}Child Content{% endbird %}
249+
{% endblock %}
250+
""")
251+
252+
template = create_template(child_path)
253+
template.render({})
254+
255+
css_assets = assets.get_assets(AssetType.CSS)
256+
asset_paths = {str(asset.path) for asset in css_assets}
257+
258+
assert str(parent_css) in asset_paths, "Parent CSS not found in assets"
259+
assert str(child_css) in asset_paths, "Child CSS not found in assets"
260+
261+
def test_empty_registry(self):
262+
assert len(assets.components) == 0
263+
assert len(assets.get_assets(AssetType.CSS)) == 0
264+
assert len(assets.get_assets(AssetType.JS)) == 0

0 commit comments

Comments
 (0)