Skip to content

Commit c3395cd

Browse files
create components dir and move attrs and templates (#50)
1 parent af12acf commit c3395cd

File tree

7 files changed

+171
-139
lines changed

7 files changed

+171
-139
lines changed

src/django_bird/components/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from django import template
6+
from django.template.context import Context
7+
from django.utils.safestring import SafeString
8+
from django.utils.safestring import mark_safe
9+
10+
11+
@dataclass
12+
class Attr:
13+
name: str
14+
value: str | bool
15+
16+
def render(self) -> str:
17+
if isinstance(self.value, bool) and self.value:
18+
return self.name
19+
return f'{self.name}="{self.value}"'
20+
21+
22+
@dataclass
23+
class Attrs:
24+
attrs: list[Attr]
25+
26+
@classmethod
27+
def parse(cls, raw_attrs: list[str], context: Context) -> Attrs:
28+
parsed = []
29+
for attr in raw_attrs:
30+
if "=" in attr:
31+
name, value = attr.split("=", 1)
32+
resolved_value = template.Variable(value).resolve(context)
33+
parsed.append(Attr(name=name, value=resolved_value))
34+
else:
35+
parsed.append(Attr(name=attr, value=True))
36+
return cls(parsed)
37+
38+
def flatten(self) -> SafeString:
39+
rendered = " ".join(attr.render() for attr in self.attrs)
40+
return mark_safe(rendered)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
from django_bird.conf import app_settings
4+
5+
6+
def get_template_names(name: str) -> list[str]:
7+
"""
8+
Generate a list of potential template names for a component.
9+
10+
The function searches for templates in the following order (from most specific to most general):
11+
12+
1. In a subdirectory named after the component, using the component name
13+
2. In the same subdirectory, using a fallback 'index.html'
14+
3. In parent directory for nested components
15+
4. In the base component directory, using the full component name
16+
17+
The order of names is important as it determines the template resolution priority.
18+
This order allows for both direct matches and hierarchical component structures,
19+
with more specific paths taking precedence over more general ones.
20+
21+
This order allows for:
22+
- Single file components
23+
- Multi-part components
24+
- Specific named files within component directories
25+
- Fallback default files for components
26+
27+
For example:
28+
- For an "input" component, the ordering would be:
29+
1. `{component_dir}/input/input.html`
30+
2. `{component_dir}/input/index.html`
31+
3. `{component_dir}/input.html`
32+
- For an "input.label" component:
33+
1. `{component_dir}/input/label/label.html`
34+
2. `{component_dir}/input/label/index.html`
35+
3. `{component_dir}/input/label.html`
36+
4. `{component_dir}/input.label.html`
37+
38+
Returns:
39+
list[str]: A list of potential template names in resolution order.
40+
"""
41+
template_names = []
42+
component_dirs = list(dict.fromkeys([*app_settings.COMPONENT_DIRS, "bird"]))
43+
44+
name_parts = name.split(".")
45+
path_name = "/".join(name_parts)
46+
47+
for component_dir in component_dirs:
48+
potential_names = [
49+
f"{component_dir}/{path_name}/{name_parts[-1]}.html",
50+
f"{component_dir}/{path_name}/index.html",
51+
f"{component_dir}/{path_name}.html",
52+
f"{component_dir}/{name}.html",
53+
]
54+
template_names.extend(potential_names)
55+
56+
return list(dict.fromkeys(template_names))

src/django_bird/templatetags/tags/bird.py

Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
from django_bird._typing import TagBits
1616
from django_bird._typing import override
17-
from django_bird.conf import app_settings
17+
from django_bird.components.attrs import Attrs
18+
from django_bird.components.templates import get_template_names
1819

1920
from .slot import SlotNode
2021

@@ -67,7 +68,7 @@ def __init__(self, name: str, attrs: list[str], nodelist: NodeList | None) -> No
6768
def render(self, context: Context) -> SafeString:
6869
component_name = self.get_component_name(context)
6970
component_context = self.get_component_context_data(context)
70-
template_names = self.get_template_names(component_name)
71+
template_names = get_template_names(component_name)
7172
template = select_template(template_names)
7273
return template.render(component_context)
7374

@@ -79,11 +80,11 @@ def get_component_name(self, context: Context) -> str:
7980
return name
8081

8182
def get_component_context_data(self, context: Context) -> dict[str, Any]:
83+
attrs = Attrs.parse(self.attrs, context)
8284
rendered_slots = self.render_slots(context)
83-
flat_attrs = self.flatten_attrs(context)
8485
default_slot = rendered_slots.get(self.default_slot) or context.get("slot")
8586
return {
86-
"attrs": mark_safe(flat_attrs),
87+
"attrs": attrs.flatten(),
8788
"slot": mark_safe(default_slot),
8889
"slots": {
8990
name: mark_safe(content) for name, content in rendered_slots.items()
@@ -117,70 +118,3 @@ def render_slots(self, context: Context) -> dict[str, str]:
117118
slot: template.Template("".join(content)).render(context)
118119
for slot, content in contents.items()
119120
}
120-
121-
def flatten_attrs(self, context: Context) -> str:
122-
attrs: dict[str, str | None | bool] = {}
123-
124-
for attr in self.attrs:
125-
if "=" in attr:
126-
key, value = attr.split("=", 1)
127-
attrs[key] = template.Variable(value).resolve(context)
128-
else:
129-
attrs[attr] = True
130-
131-
return " ".join(
132-
key if isinstance(value, bool) and value else f'{key}="{value}"'
133-
for key, value in attrs.items()
134-
)
135-
136-
def get_template_names(self, name: str) -> list[str]:
137-
"""
138-
Generate a list of potential template names for a component.
139-
140-
The function searches for templates in the following order (from most specific to most general):
141-
142-
1. In a subdirectory named after the component, using the component name
143-
2. In the same subdirectory, using a fallback 'index.html'
144-
3. In parent directory for nested components
145-
4. In the base component directory, using the full component name
146-
147-
The order of names is important as it determines the template resolution priority.
148-
This order allows for both direct matches and hierarchical component structures,
149-
with more specific paths taking precedence over more general ones.
150-
151-
This order allows for:
152-
- Single file components
153-
- Multi-part components
154-
- Specific named files within component directories
155-
- Fallback default files for components
156-
157-
For example:
158-
- For an "input" component, the ordering would be:
159-
1. `{component_dir}/input/input.html`
160-
2. `{component_dir}/input/index.html`
161-
3. `{component_dir}/input.html`
162-
- For an "input.label" component:
163-
1. `{component_dir}/input/label/label.html`
164-
2. `{component_dir}/input/label/index.html`
165-
3. `{component_dir}/input/label.html`
166-
4. `{component_dir}/input.label.html`
167-
168-
Returns:
169-
list[str]: A list of potential template names in resolution order.
170-
"""
171-
template_names = []
172-
component_dirs = list(dict.fromkeys([*app_settings.COMPONENT_DIRS, "bird"]))
173-
174-
name_parts = name.split(".")
175-
path_name = "/".join(name_parts)
176-
177-
for component_dir in component_dirs:
178-
potential_names = [
179-
f"{component_dir}/{path_name}/{name_parts[-1]}.html",
180-
f"{component_dir}/{path_name}/index.html",
181-
f"{component_dir}/{path_name}.html",
182-
f"{component_dir}/{self.name}.html",
183-
]
184-
template_names.extend(potential_names)
185-
186-
return list(dict.fromkeys(template_names))

tests/components/__init__.py

Whitespace-only changes.

tests/components/test_templates.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
from django.test import override_settings
5+
6+
from django_bird.components.templates import get_template_names
7+
8+
9+
@pytest.mark.parametrize(
10+
"name,component_dirs,expected",
11+
[
12+
(
13+
"button",
14+
[],
15+
[
16+
"bird/button/button.html",
17+
"bird/button/index.html",
18+
"bird/button.html",
19+
],
20+
),
21+
(
22+
"input.label",
23+
[],
24+
[
25+
"bird/input/label/label.html",
26+
"bird/input/label/index.html",
27+
"bird/input/label.html",
28+
"bird/input.label.html",
29+
],
30+
),
31+
(
32+
"button",
33+
["custom", "theme"],
34+
[
35+
"custom/button/button.html",
36+
"custom/button/index.html",
37+
"custom/button.html",
38+
"theme/button/button.html",
39+
"theme/button/index.html",
40+
"theme/button.html",
41+
"bird/button/button.html",
42+
"bird/button/index.html",
43+
"bird/button.html",
44+
],
45+
),
46+
],
47+
)
48+
def test_get_template_names(name, component_dirs, expected):
49+
with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": component_dirs}):
50+
template_names = get_template_names(name)
51+
52+
assert template_names == expected
53+
54+
55+
def test_get_template_names_invalid():
56+
template_names = get_template_names("input.label")
57+
58+
assert "bird/input/label/invalid.html" not in template_names
59+
60+
61+
def test_get_template_names_duplicates():
62+
with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": ["bird"]}):
63+
template_names = get_template_names("button")
64+
65+
template_counts = {}
66+
for template in template_names:
67+
template_counts[template] = template_counts.get(template, 0) + 1
68+
69+
for _, count in template_counts.items():
70+
assert count == 1

tests/templatetags/test_bird.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.template.base import Token
1111
from django.template.base import TokenType
1212
from django.template.exceptions import TemplateSyntaxError
13-
from django.test import override_settings
1413

1514
from django_bird.templatetags.tags.bird import END_TAG
1615
from django_bird.templatetags.tags.bird import TAG
@@ -280,70 +279,3 @@ def test_get_component_name(self, name, context, expected, create_bird_template)
280279
component_name = node.get_component_name(context=Context(context))
281280

282281
assert component_name == expected
283-
284-
@pytest.mark.parametrize(
285-
"name,component_dirs,expected",
286-
[
287-
(
288-
"button",
289-
[],
290-
[
291-
"bird/button/button.html",
292-
"bird/button/index.html",
293-
"bird/button.html",
294-
],
295-
),
296-
(
297-
"input.label",
298-
[],
299-
[
300-
"bird/input/label/label.html",
301-
"bird/input/label/index.html",
302-
"bird/input/label.html",
303-
"bird/input.label.html",
304-
],
305-
),
306-
(
307-
"button",
308-
["custom", "theme"],
309-
[
310-
"custom/button/button.html",
311-
"custom/button/index.html",
312-
"custom/button.html",
313-
"theme/button/button.html",
314-
"theme/button/index.html",
315-
"theme/button.html",
316-
"bird/button/button.html",
317-
"bird/button/index.html",
318-
"bird/button.html",
319-
],
320-
),
321-
],
322-
)
323-
def test_get_template_names(self, name, component_dirs, expected):
324-
node = BirdNode(name=name, attrs=[], nodelist=None)
325-
326-
with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": component_dirs}):
327-
template_names = node.get_template_names(node.name)
328-
329-
assert template_names == expected
330-
331-
def test_get_template_names_invalid(self):
332-
node = BirdNode(name="input.label", attrs=[], nodelist=None)
333-
334-
template_names = node.get_template_names(node.name)
335-
336-
assert "bird/input/label/invalid.html" not in template_names
337-
338-
def test_get_template_names_duplicates(self):
339-
with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": ["bird"]}):
340-
node = BirdNode(name="button", attrs=[], nodelist=None)
341-
342-
template_names = node.get_template_names(node.name)
343-
344-
template_counts = {}
345-
for template in template_names:
346-
template_counts[template] = template_counts.get(template, 0) + 1
347-
348-
for _, count in template_counts.items():
349-
assert count == 1

0 commit comments

Comments
 (0)