Skip to content

Commit b601a1d

Browse files
committed
fix #181: allow FilterExpressions in bird tags
1 parent 344c2ba commit b601a1d

File tree

8 files changed

+60
-90
lines changed

8 files changed

+60
-90
lines changed

src/django_bird/_typing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import sys
44

5+
from django.template.base import FilterExpression
6+
57
if sys.version_info >= (3, 12):
68
from typing import override as typing_override
79
else:
810
from typing_extensions import override as typing_override
911

1012
override = typing_override
1113

12-
TagBits = list[str]
14+
RawTagBits = list[str]
15+
TagBits = dict[str, FilterExpression]

src/django_bird/components.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from .conf import app_settings
2323
from .params import Param
2424
from .params import Params
25-
from .params import Value
2625
from .plugins import pm
2726
from .staticfiles import Asset
2827
from .staticfiles import AssetType
@@ -143,9 +142,9 @@ def render(self, context: Context):
143142
data_attrs = [
144143
Param(
145144
f"data-bird-{self.component.data_attribute_name}",
146-
Value(True),
145+
True,
147146
),
148-
Param("data-bird-id", Value(f'"{self.component.id}-{self.id}"')),
147+
Param("data-bird-id", f"{self.component.id}-{self.id}"),
149148
]
150149
self.params.attrs.extend(data_attrs)
151150

src/django_bird/params.py

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from typing import TYPE_CHECKING
66
from typing import Any
77

8-
from django import template
8+
from django.template.base import FilterExpression
9+
from django.template.base import VariableDoesNotExist
910
from django.template.context import Context
1011
from django.utils.safestring import SafeString
1112
from django.utils.safestring import mark_safe
@@ -55,18 +56,21 @@ def render_attrs(self, context: Context) -> SafeString:
5556
@classmethod
5657
def from_node(cls, node: BirdNode) -> Params:
5758
return cls(
58-
attrs=[Param.from_bit(bit) for bit in node.attrs],
59+
attrs=[Param(key, Value(value)) for key, value in node.attrs.items()],
5960
props=[],
6061
)
6162

6263

6364
@dataclass
6465
class Param:
6566
name: str
66-
value: Value
67+
value: Value | str | bool
6768

6869
def render_attr(self, context: Context) -> str:
69-
value = self.value.resolve(context)
70+
if isinstance(self.value, Value):
71+
value = self.value.resolve(context, is_attr=True)
72+
else:
73+
value = self.value
7074
if value is None:
7175
return ""
7276
name = self.name.replace("_", "-")
@@ -75,47 +79,21 @@ def render_attr(self, context: Context) -> str:
7579
return f'{name}="{value}"'
7680

7781
def render_prop(self, context: Context) -> str | bool | None:
78-
return self.value.resolve(context)
79-
80-
@classmethod
81-
def from_bit(cls, bit: str) -> Param:
82-
if "=" in bit:
83-
name, raw_value = bit.split("=", 1)
84-
value = Value(raw_value.strip())
85-
else:
86-
name, value = bit, Value(True)
87-
return cls(name, value)
82+
return (
83+
self.value.resolve(context) if isinstance(self.value, Value) else self.value
84+
)
8885

8986

9087
@dataclass
9188
class Value:
92-
raw: str | bool | None
93-
94-
def resolve(self, context: Context | dict[str, Any]) -> Any:
95-
match (self.raw, self.is_quoted):
96-
case (None, _):
97-
return None
98-
99-
case (str(raw_str), False) if raw_str == "False":
100-
return None
101-
case (str(raw_str), False) if raw_str == "True":
102-
return True
103-
104-
case (bool(b), _):
105-
return b if b else None
106-
107-
case (str(raw_str), False):
108-
try:
109-
return template.Variable(raw_str).resolve(context)
110-
except template.VariableDoesNotExist:
111-
return raw_str
112-
113-
case (_, True):
114-
return str(self.raw)[1:-1]
115-
116-
@property
117-
def is_quoted(self) -> bool:
118-
if self.raw is None or isinstance(self.raw, bool):
119-
return False
120-
121-
return self.raw.startswith(("'", '"')) and self.raw.endswith(self.raw[0])
89+
raw: FilterExpression
90+
91+
def resolve(self, context: Context | dict[str, Any], is_attr=False) -> Any:
92+
if is_attr and self.raw.token == "False":
93+
return None
94+
if self.raw.is_var:
95+
try:
96+
self.raw.var.resolve(context)
97+
except VariableDoesNotExist:
98+
return self.raw.token
99+
return self.raw.resolve(context)

src/django_bird/templatetags/tags/bird.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.template.base import Token
1010
from django.template.context import Context
1111

12+
from django_bird._typing import RawTagBits
1213
from django_bird._typing import TagBits
1314
from django_bird._typing import override
1415

@@ -23,7 +24,7 @@ def do_bird(parser: Parser, token: Token) -> BirdNode:
2324
raise template.TemplateSyntaxError(msg)
2425

2526
name = bits.pop(0)
26-
attrs: TagBits = []
27+
attrs: TagBits = {}
2728
isolated_context = False
2829

2930
for bit in bits:
@@ -33,13 +34,18 @@ def do_bird(parser: Parser, token: Token) -> BirdNode:
3334
case "/":
3435
continue
3536
case _:
36-
attrs.append(bit)
37+
if "=" in bit:
38+
key, value = bit.split("=")
39+
else:
40+
key = bit
41+
value = "True"
42+
attrs[key] = parser.compile_filter(value)
3743

3844
nodelist = parse_nodelist(bits, parser)
3945
return BirdNode(name, attrs, nodelist, isolated_context)
4046

4147

42-
def parse_nodelist(bits: TagBits, parser: Parser) -> NodeList | None:
48+
def parse_nodelist(bits: RawTagBits, parser: Parser) -> NodeList | None:
4349
# self-closing tag
4450
# {% bird name / %}
4551
if len(bits) > 0 and bits[-1] == "/":
@@ -55,7 +61,7 @@ class BirdNode(template.Node):
5561
def __init__(
5662
self,
5763
name: str,
58-
attrs: TagBits,
64+
attrs: dict[str, str],
5965
nodelist: NodeList | None,
6066
isolated_context: bool = False,
6167
) -> None:

src/django_bird/templatetags/tags/prop.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import final
55

66
from django import template
7+
from django.template.base import FilterExpression
78
from django.template.base import Parser
89
from django.template.base import Token
910
from django.template.context import Context
@@ -14,7 +15,7 @@
1415
TAG = "bird:prop"
1516

1617

17-
def do_prop(_parser: Parser, token: Token) -> PropNode:
18+
def do_prop(parser: Parser, token: Token) -> PropNode:
1819
_tag, *bits = token.split_contents()
1920
if not bits:
2021
msg = f"{TAG} tag requires at least one argument"
@@ -26,14 +27,14 @@ def do_prop(_parser: Parser, token: Token) -> PropNode:
2627
name, default = prop.split("=")
2728
except ValueError:
2829
name = prop
29-
default = None
30+
default = "None"
3031

31-
return PropNode(name, default, bits)
32+
return PropNode(name, parser.compile_filter(default), bits)
3233

3334

3435
@final
3536
class PropNode(template.Node):
36-
def __init__(self, name: str, default: str | None, attrs: TagBits):
37+
def __init__(self, name: str, default: FilterExpression, attrs: TagBits):
3738
self.name = name
3839
self.default = default
3940
self.attrs = attrs

tests/templatetags/test_bird.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ def test_missing_name_do_bird(self):
5353
@pytest.mark.parametrize(
5454
"params,expected_attrs",
5555
[
56-
('class="btn"', ['class="btn"']),
57-
("class='btn'", ["class='btn'"]),
58-
('class="btn" id="my-btn"', ['class="btn"', 'id="my-btn"']),
59-
("disabled", ["disabled"]),
60-
("class=dynamic", ["class=dynamic"]),
61-
("class=item.name id=user.id", ["class=item.name", "id=user.id"]),
56+
('class="btn"', {"class": '"btn"'}),
57+
("class='btn'", {"class": "'btn'"}),
58+
('class="btn" id="my-btn"', {"class": '"btn"', "id": '"my-btn"'}),
59+
("disabled", {"disabled": "True"}),
60+
("class=dynamic", {"class": "dynamic"}),
61+
("class=item.name id=user.id", {"class": "item.name", "id": "user.id"}),
6262
],
6363
)
6464
def test_attrs_do_bird(self, params, expected_attrs):
@@ -69,7 +69,9 @@ def test_attrs_do_bird(self, params, expected_attrs):
6969

7070
node = do_bird(parser, start_token)
7171

72-
assert node.attrs == expected_attrs
72+
for key, value in node.attrs.items():
73+
assert key in expected_attrs
74+
assert expected_attrs[key] == value.token
7375

7476
@pytest.mark.parametrize(
7577
"test_case",

tests/templatetags/test_prop.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ def test_do_prop(contents, expected):
2424
node = do_prop(Parser([]), start_token)
2525

2626
assert node.name == expected.name
27-
assert node.default == expected.default
28-
assert node.attrs == expected.attrs
27+
assert (node.default.token if node.default else node.default) == expected.default
28+
for node_attr, expected_attr in zip(node.attrs, expected.attrs, strict=False):
29+
assert node_attr.token == expected_attr
2930

3031

3132
def test_do_prop_no_args():

tests/test_params.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -121,26 +121,6 @@ def test_render_attr(self, param, context, expected):
121121
def test_render_prop(self, param, context, expected):
122122
assert param.render_prop(context) == expected
123123

124-
@pytest.mark.parametrize(
125-
"bit,expected",
126-
[
127-
("class='btn'", Param(name="class", value=Value("'btn'"))),
128-
('class="btn"', Param(name="class", value=Value('"btn"'))),
129-
("class=btn", Param(name="class", value=Value("btn"))),
130-
("disabled", Param(name="disabled", value=Value(True))),
131-
(
132-
"class=item.name",
133-
Param(name="class", value=Value("item.name")),
134-
),
135-
(
136-
'class="item.name"',
137-
Param(name="class", value=Value('"item.name"')),
138-
),
139-
],
140-
)
141-
def test_from_bit(self, bit, expected):
142-
assert Param.from_bit(bit) == expected
143-
144124

145125
class TestParams:
146126
@pytest.mark.parametrize(
@@ -269,11 +249,11 @@ def test_render_attrs(self, params, context, expected):
269249
"attrs,expected",
270250
[
271251
(
272-
['class="btn"'],
252+
{"class": '"btn"'},
273253
Params(attrs=[Param(name="class", value=Value('"btn"'))]),
274254
),
275255
(
276-
['class="btn"', 'id="my-btn"'],
256+
{"class": '"btn"', "id": '"my-btn"'},
277257
Params(
278258
attrs=[
279259
Param(name="class", value=Value('"btn"')),
@@ -282,15 +262,15 @@ def test_render_attrs(self, params, context, expected):
282262
),
283263
),
284264
(
285-
["disabled"],
265+
{"disabled": True},
286266
Params(attrs=[Param(name="disabled", value=Value(True))]),
287267
),
288268
(
289-
["class=dynamic"],
269+
{"class": "dynamic"},
290270
Params(attrs=[Param(name="class", value=Value("dynamic"))]),
291271
),
292272
(
293-
["class=item.name", "id=user.id"],
273+
{"class": "item.name", "id": "user.id"},
294274
Params(
295275
attrs=[
296276
Param(name="class", value=Value("item.name")),

0 commit comments

Comments
 (0)