Skip to content

Commit fb976dd

Browse files
Remove component discovery at startup (#214)
* Remove component discovery at startup This change removes the upfront component discovery at application startup and replaces it with on-demand discovery when templates are rendered. This should significantly improve startup performance for applications with many components. Key changes: - Remove ComponentRegistry.discover_components() method - Update get_component_names_used_in_template() to perform on-demand scanning - Add find_components_in_template() function for template scanning - Update BirdAssetFinder to scan components directly when needed - Update tests to work with on-demand component discovery * Fix static file collection for components with same name in different directories This commit improves how django-bird collects static assets from components: - Add from_template method to derive component name from template paths - Modify Component initialization to handle same-named components in different dirs - Update static file collection to include assets from all components regardless of precedence * fix tests * update changelog
1 parent 25d40a1 commit fb976dd

File tree

10 files changed

+335
-392
lines changed

10 files changed

+335
-392
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,18 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2727
- Added management command `generate_asset_manifest` for pre-computing template-component relationships
2828
- Added automatic fallback to component scanning in development mode (when DEBUG=True)
2929

30+
### Changed
31+
32+
- Improved static file collection to include assets from all component directories
33+
34+
### Fixed
35+
36+
- Fixed bug where assets from components with the same name in different directories weren't being properly collected
37+
3038
### Removed
3139

3240
- Removed automatic component scanning at application startup for faster initialization
41+
- **Internal**: Removed `ComponentRegistry.discover_components` method.
3342

3443
## [0.16.2]
3544

src/django_bird/components.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@
2626
from .plugins import pm
2727
from .staticfiles import Asset
2828
from .staticfiles import AssetType
29-
from .templates import gather_bird_tag_template_usage
29+
from .templates import find_components_in_template
3030
from .templates import get_component_directories
3131
from .templates import get_template_names
3232
from .templatetags.tags.bird import BirdNode
3333
from .templatetags.tags.slot import DEFAULT_SLOT
3434
from .templatetags.tags.slot import SlotNode
35-
from .utils import get_files_from_dirs
3635

3736

3837
@dataclass(frozen=True, slots=True)
@@ -76,17 +75,34 @@ def source(self):
7675
return self.template.template.source
7776

7877
@classmethod
79-
def from_abs_path(cls, path: Path, root: Path) -> Component:
80-
name = str(path.relative_to(root).with_suffix("")).replace("/", ".")
81-
return cls.from_name(name)
78+
def from_abs_path(cls, path: Path) -> Component:
79+
template = select_template([str(path)])
80+
return cls.from_template(template)
8281

8382
@classmethod
84-
def from_name(cls, name: str):
83+
def from_name(cls, name: str) -> Component:
8584
template_names = get_template_names(name)
8685
template = select_template(template_names)
86+
return cls.from_template(template)
87+
88+
@classmethod
89+
def from_template(cls, template: DjangoTemplate) -> Component:
90+
template_path = Path(template.template.origin.name)
91+
92+
for component_dir in get_component_directories():
93+
try:
94+
relative_path = template_path.relative_to(component_dir)
95+
name = str(relative_path.with_suffix("")).replace("/", ".")
96+
break
97+
except ValueError:
98+
continue
99+
else:
100+
name = template_path.stem
101+
87102
assets: list[Iterable[Asset]] = pm.hook.collect_component_assets(
88103
template_path=Path(template.template.origin.name)
89104
)
105+
90106
return cls(
91107
name=name, template=template, assets=frozenset(itertools.chain(*assets))
92108
)
@@ -182,24 +198,6 @@ def __init__(self):
182198
self._components: dict[str, Component] = {}
183199
self._template_usage: dict[Path, set[str]] = defaultdict(set)
184200

185-
def discover_components(self) -> None:
186-
component_dirs = get_component_directories()
187-
component_paths = get_files_from_dirs(component_dirs)
188-
for component_abs_path, root_abs_path in component_paths:
189-
if component_abs_path.suffix != ".html":
190-
continue
191-
192-
component = Component.from_abs_path(component_abs_path, root_abs_path)
193-
194-
if component.name not in self._components:
195-
self._components[component.name] = component
196-
197-
templates_using_bird_tag = gather_bird_tag_template_usage()
198-
for template_abs_path, component_names in templates_using_bird_tag:
199-
self._template_usage[template_abs_path] = component_names
200-
for component_name in component_names:
201-
self._component_usage[component_name].add(template_abs_path)
202-
203201
def reset(self) -> None:
204202
"""Reset the registry, used for testing."""
205203
self._component_usage = defaultdict(set)
@@ -227,8 +225,19 @@ def get_component_names_used_in_template(
227225
self, template_path: str | Path
228226
) -> set[str]:
229227
"""Get names of components used in a template."""
230-
path = Path(template_path) if isinstance(template_path, str) else template_path
231-
return self._template_usage.get(path, set())
228+
229+
path = Path(template_path)
230+
231+
if path in self._template_usage:
232+
return self._template_usage[path]
233+
234+
components = find_components_in_template(template_path)
235+
236+
self._template_usage[path] = components
237+
for component_name in components:
238+
self._component_usage[component_name].add(path)
239+
240+
return components
232241

233242
def get_component_usage(
234243
self, template_path: str | Path

src/django_bird/staticfiles.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

3+
import logging
34
from collections.abc import Callable
45
from collections.abc import Iterable
5-
from collections.abc import Sequence
66
from dataclasses import dataclass
77
from enum import Enum
88
from pathlib import Path
@@ -25,6 +25,9 @@
2525
from .conf import app_settings
2626
from .templates import get_component_directories
2727
from .templatetags.tags.asset import AssetTag
28+
from .utils import get_files_from_dirs
29+
30+
logger = logging.getLogger(__name__)
2831

2932
if TYPE_CHECKING:
3033
from .components import Component
@@ -174,14 +177,6 @@ def __init__(self, *args: Any, prefix: str, **kwargs: Any):
174177

175178
@final
176179
class BirdAssetFinder(BaseFinder):
177-
def __init__(
178-
self, app_names: Sequence[str] | None = None, *args: Any, **kwargs: Any
179-
) -> None:
180-
from .components import components
181-
182-
self.components = components
183-
super().__init__(*args, **kwargs)
184-
185180
@override
186181
def check(self, **kwargs: Any) -> list[CheckMessage]:
187182
return []
@@ -237,12 +232,34 @@ def list(
237232
) -> Iterable[tuple[str, FileSystemStorage]]:
238233
"""
239234
Return (relative_path, storage) pairs for all assets.
235+
236+
This method is used by Django's collectstatic command to find
237+
all assets that should be collected.
240238
"""
241-
self.components.discover_components()
242239

243-
for asset in self.components.get_assets():
244-
if ignore_patterns and any(
245-
asset.relative_path.match(pattern) for pattern in set(ignore_patterns)
246-
):
240+
from django_bird.components import Component
241+
242+
component_dirs = get_component_directories()
243+
244+
for path, _ in get_files_from_dirs(component_dirs):
245+
if path.suffix != ".html":
246+
continue
247+
248+
try:
249+
component = Component.from_abs_path(path)
250+
251+
for asset in component.assets:
252+
if ignore_patterns and any(
253+
asset.relative_path.match(pattern)
254+
for pattern in set(ignore_patterns)
255+
):
256+
logger.debug(
257+
f"Skipping asset {asset.path} due to ignore pattern"
258+
)
259+
continue
260+
261+
yield str(asset.relative_path), asset.storage
262+
263+
except Exception as e:
264+
logger.error(f"Error loading component {path}: {e}")
247265
continue
248-
yield str(asset.relative_path), asset.storage

src/django_bird/templates.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,34 @@ def _process_template_chunk( # pragma: no cover
130130
) -> list[tuple[Path, set[str]]]:
131131
results: list[tuple[Path, set[str]]] = []
132132
for path, root in templates:
133-
visitor = NodeVisitor(Engine.get_default())
134133
template_name = str(path.relative_to(root))
135-
try:
136-
template = Engine.get_default().get_template(template_name)
137-
except (TemplateDoesNotExist, TemplateSyntaxError):
138-
continue
134+
components = find_components_in_template(template_name)
135+
if components:
136+
results.append((path, components))
137+
return results
138+
139+
140+
def find_components_in_template(template_path: str | Path) -> set[str]:
141+
"""Find all component names used in a specific template.
142+
143+
Args:
144+
template_path: Path to the template file or template name
145+
146+
Returns:
147+
set[str]: Set of component names used in the template
148+
"""
149+
template_name = str(template_path)
150+
151+
visitor = NodeVisitor(Engine.get_default())
152+
try:
153+
template = Engine.get_default().get_template(template_name)
139154
context = Context()
140155
with context.bind_template(template):
141156
visitor.visit(template, context)
142-
if visitor.components:
143-
results.append((path, visitor.components))
144-
return results
157+
return visitor.components
158+
except (TemplateDoesNotExist, TemplateSyntaxError):
159+
# If we can't load the template, return an empty set
160+
return set()
145161

146162

147163
NodeVisitorMethod = Callable[[Template | Node, Context], None]

tests/templatetags/test_asset.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@ def test_template_inheritence(self, create_template, templates_dir, registry):
9898
{% endblock %}
9999
""")
100100

101-
registry.discover_components()
102-
103101
template = create_template(child_path)
104102

105103
rendered = template.render({})
@@ -151,8 +149,6 @@ def test_template_inheritence_no_bird_usage(
151149
{% endblock %}
152150
""")
153151

154-
registry.discover_components()
155-
156152
template = create_template(child_path)
157153

158154
rendered = template.render({})
@@ -230,8 +226,6 @@ def test_component_render_order(self, create_template, templates_dir, registry):
230226
</html>
231227
""")
232228

233-
registry.discover_components()
234-
235229
template = create_template(template_path)
236230

237231
rendered = template.render({})
@@ -294,8 +288,6 @@ def test_asset_duplication(self, create_template, templates_dir, registry):
294288
{% endblock %}
295289
""")
296290

297-
registry.discover_components()
298-
299291
template = get_template(child_path.name)
300292

301293
rendered = template.render({})
@@ -359,8 +351,6 @@ def test_unused_component_asset_not_rendered(
359351
{% endblock %}
360352
""")
361353

362-
registry.discover_components()
363-
364354
template = create_template(child_path)
365355

366356
rendered = template.render({})
@@ -424,8 +414,6 @@ def test_asset_tag_with_manifest_in_production(
424414
</html>
425415
""")
426416

427-
registry.discover_components()
428-
429417
manifest_data = generate_asset_manifest()
430418

431419
manifest_path = static_root / "django_bird"
@@ -467,8 +455,6 @@ def test_asset_tag_in_debug_mode(
467455
</html>
468456
""")
469457

470-
registry.discover_components()
471-
472458
manifest_data = {"not/a/real/template.html": ["not-button"]}
473459

474460
manifest_path = static_root / "django_bird"
@@ -516,8 +502,6 @@ def test_asset_tag_fallback_when_template_not_in_manifest(
516502
<html><body>Other template</body></html>
517503
""")
518504

519-
registry.discover_components()
520-
521505
manifest_data = {str(other_path): ["button"]}
522506

523507
manifest_path = static_root / "django_bird"

0 commit comments

Comments
 (0)