Skip to content

Commit d0bcca0

Browse files
Refactor Component loading strategy in ComponentRegistry (#148)
1 parent a5a7277 commit d0bcca0

File tree

13 files changed

+325
-214
lines changed

13 files changed

+325
-214
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
2727
### Changed
2828

2929
- **Internal**: Renamed `TemplateConfigurator` to `AutoConfigurator` and consolidated configuration logic.
30+
- **Internal**: Refactored component and asset loading strategy to track relationships between templates and components, affecting `ComponentRegistry.discover_components` and the `{% bird:asset %}` templatetag.
3031

3132
### Deprecated
3233

@@ -35,6 +36,12 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
3536
### Removed
3637

3738
- Removed the deprecated `ENABLE_BIRD_ID_ATTR` setting.
39+
- Removed automatically discovering templates in `BASE_DIR/templates`. Templates must now be in directories configured in Django's template engine settings or app template directories.
40+
- Removed component scanning functionality from `BirdLoader`.
41+
42+
### Fixed
43+
44+
- Fixed asset loading in `{% bird:asset %}` templatetags to only render assets from components actually used in the current template by tracking template-component relationships during component discovery.
3845

3946
## [0.12.1]
4047

src/django_bird/components.py

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

3+
from collections import defaultdict
4+
from collections.abc import Generator
35
from dataclasses import dataclass
46
from dataclasses import field
57
from hashlib import md5
68
from pathlib import Path
79
from threading import Lock
8-
from typing import TYPE_CHECKING
10+
from typing import Any
911

1012
from cachetools import LRUCache
1113
from django.conf import settings
@@ -14,23 +16,22 @@
1416
from django.template.base import NodeList
1517
from django.template.base import TextNode
1618
from django.template.context import Context
17-
from django.template.exceptions import TemplateDoesNotExist
1819
from django.template.loader import select_template
1920

20-
from django_bird.params import Param
21-
from django_bird.params import Params
22-
from django_bird.params import Value
23-
from django_bird.staticfiles import Asset
24-
from django_bird.templatetags.tags.slot import DEFAULT_SLOT
25-
from django_bird.templatetags.tags.slot import SlotNode
26-
2721
from .conf import app_settings
22+
from .params import Param
23+
from .params import Params
24+
from .params import Value
25+
from .staticfiles import Asset
2826
from .staticfiles import AssetType
27+
from .templates import gather_bird_tag_template_usage
2928
from .templates import get_component_directories
29+
from .templates import get_files_from_dirs
3030
from .templates import get_template_names
31-
32-
if TYPE_CHECKING:
33-
from django_bird.templatetags.tags.bird import BirdNode
31+
from .templates import scan_template_for_bird_tag
32+
from .templatetags.tags.bird import BirdNode
33+
from .templatetags.tags.slot import DEFAULT_SLOT
34+
from .templatetags.tags.slot import SlotNode
3435

3536

3637
@dataclass(frozen=True, slots=True)
@@ -73,6 +74,15 @@ def path(self):
7374
def source(self):
7475
return self.template.template.source
7576

77+
@property
78+
def used_in(self):
79+
return components.get_template_usage(self.name)
80+
81+
@classmethod
82+
def from_abs_path(cls, path: Path, root: Path) -> Component | None:
83+
name = str(path.relative_to(root).with_suffix("")).replace("/", ".")
84+
return cls.from_name(name)
85+
7686
@classmethod
7787
def from_name(cls, name: str):
7888
template_names = get_template_names(name)
@@ -180,41 +190,39 @@ def id(self):
180190

181191
class ComponentRegistry:
182192
def __init__(self, maxsize: int = 100):
193+
self._component_usage: dict[str, set[Path]] = defaultdict(set)
183194
self._components: LRUCache[str, Component] = LRUCache(maxsize=maxsize)
195+
self._template_usage: dict[Path, set[str]] = defaultdict(set)
184196

185197
def discover_components(self) -> None:
186198
component_dirs = get_component_directories()
199+
component_paths = get_files_from_dirs(component_dirs)
200+
for component_abs_path, root_abs_path in component_paths:
201+
if component_abs_path.suffix != ".html":
202+
continue
187203

188-
for component_dir in component_dirs:
189-
component_dir = Path(component_dir)
190-
191-
if not component_dir.is_dir():
204+
component = Component.from_abs_path(component_abs_path, root_abs_path)
205+
if component is None:
192206
continue
193207

194-
for component_path in component_dir.rglob("*.html"):
195-
component_name = str(
196-
component_path.relative_to(component_dir).with_suffix("")
197-
).replace("/", ".")
198-
try:
199-
component = Component.from_name(component_name)
200-
self._components[component_name] = component
201-
except TemplateDoesNotExist:
202-
continue
203-
204-
def clear(self) -> None:
205-
"""Clear the registry. Mainly useful for testing."""
206-
self._components.clear()
208+
if component.name not in self._components:
209+
self._components[component.name] = component
207210

208-
def get_component(self, name: str) -> Component:
209-
try:
210-
if not settings.DEBUG:
211-
return self._components[name]
212-
except KeyError:
213-
pass
211+
templates_using_bird_tag = gather_bird_tag_template_usage()
212+
for template_abs_path, root_abs_path in templates_using_bird_tag:
213+
if self._template_usage.get(template_abs_path, None) is not None:
214+
continue
214215

215-
component = Component.from_name(name)
216-
self._components[name] = component
217-
return component
216+
template_name = template_abs_path.relative_to(root_abs_path)
217+
for component_name in scan_template_for_bird_tag(str(template_name)):
218+
self._template_usage[template_abs_path].add(component_name)
219+
self._component_usage[component_name].add(template_abs_path)
220+
221+
def reset(self) -> None:
222+
"""Reset the registry, used for testing."""
223+
self._component_usage = defaultdict(set)
224+
self._components.clear()
225+
self._template_usage = defaultdict(set)
218226

219227
def get_assets(self, asset_type: AssetType | None = None) -> frozenset[Asset]:
220228
return frozenset(
@@ -224,5 +232,25 @@ def get_assets(self, asset_type: AssetType | None = None) -> frozenset[Asset]:
224232
if asset_type is None or asset.type == asset_type
225233
)
226234

235+
def get_component(self, name: str) -> Component:
236+
if name in self._components and not settings.DEBUG:
237+
return self._components[name]
238+
239+
self._components[name] = Component.from_name(name)
240+
if name not in self._component_usage:
241+
self._component_usage[name] = set()
242+
return self._components[name]
243+
244+
def get_component_usage(
245+
self, template_path: str | Path
246+
) -> Generator[Component, Any, None]:
247+
path = Path(template_path) if isinstance(template_path, str) else template_path
248+
for component_name in self._template_usage.get(path, set()):
249+
yield Component.from_name(component_name)
250+
251+
def get_template_usage(self, component: str | Component) -> frozenset[Path]:
252+
name = component.name if isinstance(component, Component) else component
253+
return frozenset(self._component_usage.get(name, set()))
254+
227255

228256
components = ComponentRegistry()

src/django_bird/loader.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,13 @@
44
import re
55

66
from django.core.cache import cache
7-
from django.template.base import Node
87
from django.template.base import Origin
9-
from django.template.base import Template
10-
from django.template.context import Context
118
from django.template.engine import Engine
12-
from django.template.loader_tags import ExtendsNode
13-
from django.template.loader_tags import IncludeNode
149
from django.template.loaders.filesystem import Loader as FileSystemLoader
1510

16-
from ._typing import _has_nodelist
1711
from ._typing import override
1812
from .compiler import Compiler
19-
from .components import components
2013
from .templatetags.tags.bird import TAG
21-
from .templatetags.tags.bird import BirdNode
2214

2315
BIRD_TAG_PATTERN = re.compile(rf"{{%\s*{TAG}\s+([^\s%}}]+)")
2416

@@ -35,40 +27,9 @@ def get_contents(self, origin: Origin) -> str:
3527
if not BIRD_TAG_PATTERN.search(contents):
3628
return contents
3729

38-
template = Template(contents, origin=origin, engine=self.engine)
39-
context = Context()
40-
with context.bind_template(template):
41-
self._ensure_components_loaded(template, context)
42-
4330
cache_key = f"bird_component_{hashlib.md5(contents.encode()).hexdigest()}"
4431
compiled = cache.get(cache_key)
4532
if compiled is None:
4633
compiled = self.compiler.compile(contents)
4734
cache.set(cache_key, compiled, timeout=None)
4835
return compiled
49-
50-
def _ensure_components_loaded(
51-
self, node: Template | Node, context: Context
52-
) -> None:
53-
"""Ensure all components used in the template are loaded."""
54-
if isinstance(node, BirdNode):
55-
components.get_component(node.name)
56-
57-
if not _has_nodelist(node) or node.nodelist is None:
58-
return
59-
60-
for child in node.nodelist:
61-
if isinstance(child, BirdNode):
62-
components.get_component(child.name)
63-
64-
if isinstance(child, ExtendsNode):
65-
parent_template = child.get_parent(context)
66-
self._ensure_components_loaded(parent_template, context)
67-
68-
if isinstance(child, IncludeNode):
69-
template_name = child.template.token.strip("'\"")
70-
included_template = self.engine.get_template(template_name)
71-
self._ensure_components_loaded(included_template, context)
72-
73-
if hasattr(child, "nodelist"):
74-
self._ensure_components_loaded(child, context)

0 commit comments

Comments
 (0)