Skip to content

Commit 4fcf34f

Browse files
add assets field to Component dataclass and collect assets (#65)
* add assets field to `Component` dataclass and collect assets * update changelog
1 parent c0a5191 commit 4fcf34f

File tree

6 files changed

+140
-181
lines changed

6 files changed

+140
-181
lines changed

CHANGELOG.md

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

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Component assets (CSS/JS) are now automatically discovered and associated with components
24+
25+
### Changed
26+
27+
- **Internal**: Refactored `AssetType` to use string values and file extensions
28+
29+
### Removed
30+
31+
- **Internal**: Simplified asset handling by removing global registry in favor of per-component assets
32+
2133
## [0.5.0]
2234

2335
### Added

src/django_bird/components.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from dataclasses import field
45
from typing import Any
56

67
from cachetools import LRUCache
78
from django.template.backends.django import Template
89
from django.template.loader import select_template
910

11+
from django_bird.staticfiles import Asset
12+
13+
from .staticfiles import get_template_assets
1014
from .templates import get_template_names
1115

1216

1317
@dataclass(frozen=True, slots=True)
1418
class Component:
1519
name: str
1620
template: Template
21+
assets: set[Asset] = field(default_factory=set)
1722

1823
def render(self, context: dict[str, Any]):
1924
return self.template.render(context)
@@ -26,7 +31,8 @@ def nodelist(self):
2631
def from_name(cls, name: str):
2732
template_names = get_template_names(name)
2833
template = select_template(template_names)
29-
return cls(name=name, template=template)
34+
assets = get_template_assets(template)
35+
return cls(name=name, template=template, assets=assets)
3036

3137

3238
class Registry:

src/django_bird/staticfiles.py

Lines changed: 19 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
from __future__ import annotations
22

3-
from collections.abc import Callable
43
from dataclasses import dataclass
5-
from enum import IntEnum
4+
from enum import Enum
65
from pathlib import Path
76

8-
from django.templatetags.static import static
9-
from django.utils.html import format_html
10-
from django.utils.html import format_html_join
11-
from django.utils.safestring import SafeString
7+
from django.template.backends.django import Template
128

139
from ._typing import override
1410

1511

16-
class AssetType(IntEnum):
17-
CSS = 1
18-
JS = 2
12+
class AssetType(Enum):
13+
CSS = "css"
14+
JS = "js"
1915

16+
@property
17+
def ext(self):
18+
return f".{self.value}"
2019

21-
@dataclass(frozen=True)
20+
21+
@dataclass(frozen=True, slots=True)
2222
class Asset:
2323
path: Path
2424
type: AssetType
@@ -42,52 +42,12 @@ def from_path(cls, path: Path) -> Asset:
4242
return cls(path=path, type=asset_type)
4343

4444

45-
class Registry:
46-
def __init__(self) -> None:
47-
self.assets: set[Asset] = set()
48-
49-
def register(self, asset: Asset | Path) -> None:
50-
if isinstance(asset, Path):
51-
asset = Asset.from_path(asset)
52-
53-
if not asset.exists():
54-
raise FileNotFoundError(f"Asset file not found: {asset.path}")
55-
56-
self.assets.add(asset)
57-
58-
def clear(self) -> None:
59-
self.assets.clear()
60-
61-
def get_assets(self, asset_type: AssetType) -> list[Asset]:
62-
assets = [asset for asset in self.assets if asset.type == asset_type]
63-
return self.sort_assets(assets)
64-
65-
def sort_assets(
66-
self,
67-
assets: list[Asset],
68-
*,
69-
key: Callable[[Asset], str] = lambda a: str(a.path),
70-
) -> list[Asset]:
71-
return sorted(assets, key=key)
72-
73-
def get_format_string(self, asset_type: AssetType) -> str:
74-
match asset_type:
75-
case AssetType.CSS:
76-
return '<link rel="stylesheet" href="{}" type="text/css">'
77-
case AssetType.JS:
78-
return '<script src="{}" type="text/javascript">'
79-
80-
def render(self, asset_type: AssetType) -> SafeString:
81-
assets = self.get_assets(asset_type)
82-
83-
if not assets:
84-
return format_html("")
85-
86-
return format_html_join(
87-
"\n",
88-
self.get_format_string(asset_type),
89-
((static(str(asset.path)),) for asset in assets),
90-
)
91-
92-
93-
registry = Registry()
45+
def get_template_assets(template: Template):
46+
assets: set[Asset] = set()
47+
template_path = Path(template.template.origin.name)
48+
for asset_type in AssetType:
49+
asset_path = template_path.with_suffix(asset_type.ext)
50+
if asset_path.exists():
51+
asset = Asset.from_path(asset_path)
52+
assets.add(asset)
53+
return assets

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import pytest
88
from django.conf import settings
9+
from django.template.backends.django import Template as DjangoTemplate
10+
from django.template.engine import Engine
911
from django.test import override_settings
1012

1113
from .settings import DEFAULT_SETTINGS
@@ -94,6 +96,16 @@ def func(name, content, sub_dir=None, bird_dir_name="bird"):
9496
return func
9597

9698

99+
@pytest.fixture
100+
def create_template():
101+
def _create_template(template_file: Path) -> DjangoTemplate:
102+
engine = Engine.get_default()
103+
django_template = engine.get_template(str(template_file))
104+
return DjangoTemplate(django_template, engine)
105+
106+
return _create_template
107+
108+
97109
@pytest.fixture
98110
def normalize_whitespace():
99111
def func(text):

tests/test_components.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,62 @@
66

77
from django_bird.components import Component
88
from django_bird.components import Registry
9+
from django_bird.staticfiles import Asset
10+
from django_bird.staticfiles import AssetType
911

1012

1113
class TestComponent:
12-
def test_from_name(self, create_bird_template):
14+
def test_from_name_basic(self, create_bird_template):
1315
create_bird_template("button", "<button>Click me</button>")
1416

1517
comp = Component.from_name("button")
1618

1719
assert comp.name == "button"
20+
assert comp.assets == set()
1821
assert isinstance(comp.template, Template)
1922
assert comp.render({}) == "<button>Click me</button>"
2023

24+
def test_from_name_with_assets(self, create_template, create_bird_template):
25+
template_file = create_bird_template("button", "<button>Click me</button>")
26+
create_template(template_file)
27+
28+
css_file = template_file.with_suffix(".css")
29+
js_file = template_file.with_suffix(".js")
30+
css_file.write_text("button { color: red; }")
31+
js_file.write_text("console.log('loaded');")
32+
33+
comp = Component.from_name("button")
34+
35+
assert len(comp.assets) == 2
36+
assert Asset(css_file, AssetType.CSS) in comp.assets
37+
assert Asset(js_file, AssetType.JS) in comp.assets
38+
39+
@pytest.mark.parametrize(
40+
"asset_suffix,asset_content,expected_asset_type",
41+
[
42+
(".css", "button { color: red; }", AssetType.CSS),
43+
(".js", "console.log('loaded');", AssetType.JS),
44+
],
45+
)
46+
def test_from_name_with_partial_assets(
47+
self,
48+
asset_suffix,
49+
asset_content,
50+
expected_asset_type,
51+
create_template,
52+
create_bird_template,
53+
):
54+
template_file = create_bird_template("button", "<button>Click me</button>")
55+
create_template(template_file)
56+
57+
file = template_file.with_suffix(asset_suffix)
58+
file.write_text(asset_content)
59+
60+
comp = Component.from_name("button")
61+
62+
assert len(comp.assets) == 1
63+
assert Asset(file, expected_asset_type) in comp.assets
64+
2165

2266
class TestRegistry:
2367
@pytest.fixture

0 commit comments

Comments
 (0)