Skip to content

Commit b640f87

Browse files
Move component discovery to registry and run at startup (#88)
1 parent 639204d commit b640f87

File tree

8 files changed

+120
-7
lines changed

8 files changed

+120
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2121
### Changed
2222

2323
- **Internal**: Consolidated Component and Asset registries into a single `ComponentRegistry`.
24+
- **Internal**: Added component discovery at app startup instead of on-demand in the template loader.
2425

2526
## [0.7.2]
2627

src/django_bird/apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class DjangoBirdAppConfig(AppConfig):
1212

1313
@override
1414
def ready(self):
15+
from django_bird.components import components
1516
from django_bird.conf import app_settings
1617

1718
app_settings.autoconfigure()
19+
components.discover_components()

src/django_bird/components.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from cachetools import LRUCache
99
from django.conf import settings
1010
from django.template.backends.django import Template as DjangoTemplate
11+
from django.template.exceptions import TemplateDoesNotExist
1112
from django.template.loader import select_template
1213

1314
from django_bird.staticfiles import Asset
1415

16+
from .conf import app_settings
1517
from .staticfiles import AssetType
1618
from .templates import get_template_names
1719

@@ -49,6 +51,32 @@ class ComponentRegistry:
4951
def __init__(self, maxsize: int = 100):
5052
self._components: LRUCache[str, Component] = LRUCache(maxsize=maxsize)
5153

54+
def discover_components(self) -> None:
55+
template_dirs = [dir for config in settings.TEMPLATES for dir in config["DIRS"]]
56+
57+
component_dirs = [
58+
Path(template_dir) / component_dir
59+
for template_dir in template_dirs
60+
for component_dir in app_settings.COMPONENT_DIRS + ["bird"]
61+
]
62+
63+
for component_dir in component_dirs:
64+
if not component_dir.is_dir():
65+
continue
66+
67+
for component_path in component_dir.iterdir():
68+
if component_path.is_dir():
69+
component_name = component_path.name
70+
elif component_path.suffix == ".html":
71+
component_name = component_path.stem
72+
else:
73+
continue
74+
75+
try:
76+
self.get_component(component_name)
77+
except TemplateDoesNotExist:
78+
continue
79+
5280
def clear(self) -> None:
5381
"""Clear the registry. Mainly useful for testing."""
5482
self._components.clear()

src/django_bird/loader.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def get_contents(self, origin: Origin) -> str:
3838
template = Template(contents, origin=origin, engine=self.engine)
3939
context = Context()
4040
with context.bind_template(template):
41-
self._scan_for_components(template, context)
41+
self._ensure_components_loaded(template, context)
4242

4343
cache_key = f"bird_component_{hashlib.md5(contents.encode()).hexdigest()}"
4444
compiled = cache.get(cache_key)
@@ -47,7 +47,10 @@ def get_contents(self, origin: Origin) -> str:
4747
cache.set(cache_key, compiled, timeout=None)
4848
return compiled
4949

50-
def _scan_for_components(self, node: Template | Node, context: Context) -> None:
50+
def _ensure_components_loaded(
51+
self, node: Template | Node, context: Context
52+
) -> None:
53+
"""Ensure all components used in the template are loaded."""
5154
if isinstance(node, BirdNode):
5255
components.get_component(node.name)
5356

@@ -60,12 +63,12 @@ def _scan_for_components(self, node: Template | Node, context: Context) -> None:
6063

6164
if isinstance(child, ExtendsNode):
6265
parent_template = child.get_parent(context)
63-
self._scan_for_components(parent_template, context)
66+
self._ensure_components_loaded(parent_template, context)
6467

6568
if isinstance(child, IncludeNode):
6669
template_name = child.template.token.strip("'\"")
6770
included_template = self.engine.get_template(template_name)
68-
self._scan_for_components(included_template, context)
71+
self._ensure_components_loaded(included_template, context)
6972

7073
if hasattr(child, "nodelist"):
71-
self._scan_for_components(child, context)
74+
self._ensure_components_loaded(child, context)

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import contextlib
34
import logging
45
import re
56
from pathlib import Path
@@ -71,6 +72,18 @@ def override_templates_settings(templates_dir):
7172
yield
7273

7374

75+
@pytest.fixture
76+
def override_app_settings():
77+
from django_bird.conf import DJANGO_BIRD_SETTINGS_NAME
78+
79+
@contextlib.contextmanager
80+
def _override_app_settings(**kwargs):
81+
with override_settings(**{DJANGO_BIRD_SETTINGS_NAME: {**kwargs}}):
82+
yield
83+
84+
return _override_app_settings
85+
86+
7487
@pytest.fixture
7588
def create_bird_dir(templates_dir):
7689
def func(name):

tests/test_apps.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
from django.apps import apps
4+
5+
from django_bird.components import components
6+
7+
8+
def test_ready_scans_components(create_bird_template):
9+
create_bird_template("button", "<button>Click me</button>")
10+
create_bird_template("alert", "<div>Alert</div>")
11+
12+
components.clear()
13+
14+
assert "button" not in components._components
15+
assert "alert" not in components._components
16+
17+
apps.get_app_config("django_bird").ready()
18+
19+
assert "button" in components._components
20+
assert "alert" in components._components

tests/test_components.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,52 @@ class TestComponentRegistry:
9191
def registry(self):
9292
return ComponentRegistry(maxsize=2)
9393

94+
def test_initialize_loads_components(
95+
self, registry, create_bird_template, settings
96+
):
97+
create_bird_template("button", "<button>Click me</button>")
98+
create_bird_template("alert", "<div>Alert</div>")
99+
100+
registry.discover_components()
101+
102+
assert "button" in registry._components
103+
assert "alert" in registry._components
104+
105+
def test_initialize_loads_assets(
106+
self, registry, create_bird_template, create_bird_asset
107+
):
108+
template = create_bird_template("button", "<button>Click me</button>")
109+
create_bird_asset(template, ".button { color: red; }", "css")
110+
create_bird_asset(template, "console.log('button');", "js")
111+
112+
registry.discover_components()
113+
114+
component = registry._components["button"]
115+
assert len(component.assets) == 2
116+
117+
def test_initialize_with_custom_dirs(
118+
self, registry, create_bird_template, override_app_settings
119+
):
120+
create_bird_template(
121+
"button", "<button>Click me</button>", bird_dir_name="components"
122+
)
123+
124+
with override_app_settings(COMPONENT_DIRS=["components"]):
125+
registry.discover_components()
126+
127+
assert "button" in registry._components
128+
129+
def test_initialize_handles_missing_dirs(self, registry, settings):
130+
settings.COMPONENT_DIRS = ["nonexistent"]
131+
132+
registry.discover_components()
133+
134+
def test_initialize_handles_invalid_components(self, registry, tmp_path, settings):
135+
component_dir = tmp_path / "bird" / "invalid"
136+
component_dir.mkdir(parents=True)
137+
138+
registry.discover_components()
139+
94140
def test_get_component_caches(self, registry, create_bird_template):
95141
create_bird_template(name="button", content="<button>Click me</button>")
96142

tests/test_loader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_render_template(template_name):
7474
(Template("{% bird button %}{% bird button %}{% endbird %}{% endbird %}"), 1),
7575
],
7676
)
77-
def test_scan_for_components(
77+
def test_ensure_components_loaded(
7878
node, expected_count, create_bird_template, create_bird_asset
7979
):
8080
button = create_bird_template("button", "<button>Click me</button>")
@@ -84,6 +84,6 @@ def test_scan_for_components(
8484
loader = BirdLoader(Engine.get_default())
8585
context = Context()
8686

87-
loader._scan_for_components(node, context)
87+
loader._ensure_components_loaded(node, context)
8888

8989
assert len(components._components) == expected_count

0 commit comments

Comments
 (0)