-
-
Notifications
You must be signed in to change notification settings - Fork 3
fix #181: allow FilterExpressions in bird tags #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,8 @@ | |
from typing import TYPE_CHECKING | ||
from typing import Any | ||
|
||
from django import template | ||
from django.template.base import FilterExpression | ||
from django.template.base import VariableDoesNotExist | ||
from django.template.context import Context | ||
from django.utils.safestring import SafeString | ||
from django.utils.safestring import mark_safe | ||
|
@@ -55,18 +56,21 @@ def render_attrs(self, context: Context) -> SafeString: | |
@classmethod | ||
def from_node(cls, node: BirdNode) -> Params: | ||
return cls( | ||
attrs=[Param.from_bit(bit) for bit in node.attrs], | ||
attrs=[Param(key, Value(value)) for key, value in node.attrs.items()], | ||
props=[], | ||
) | ||
|
||
|
||
@dataclass | ||
class Param: | ||
name: str | ||
value: Value | ||
value: Value | str | bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HM! Feels like the abstraction is leaking a bit here. I've become a recent convert to the thin dataclasses with more explicit names and a tagged union @dataclass
class LiteralValue:
raw: str
def resolve(self, context: Context) -> str:
return self.raw
@dataclass
class BooleanValue:
value: bool
def resolve(self, context: Context) -> bool | None:
return self.value if self.value else None
@dataclass
class ExpressionValue:
expression: FilterExpression
def resolve(self, context: Context) -> Any:
try:
return self.expression.resolve(context)
except VariableDoesNotExist:
# Handle missing variables gracefully
if self.expression.is_var and not self.expression.filters:
return self.expression.token
raise
Value = LiteralValue | BooleanValue | ExpressionValue Looking through the rest of the code, I think this would be compatible with the rest of the calls, though that would obviously need to be tested. |
||
|
||
def render_attr(self, context: Context) -> str: | ||
value = self.value.resolve(context) | ||
if isinstance(self.value, Value): | ||
value = self.value.resolve(context, is_attr=True) | ||
else: | ||
value = self.value | ||
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the tagged union approach is taken, this could just be reverted. |
||
if value is None: | ||
return "" | ||
name = self.name.replace("_", "-") | ||
|
@@ -75,47 +79,21 @@ def render_attr(self, context: Context) -> str: | |
return f'{name}="{value}"' | ||
|
||
def render_prop(self, context: Context) -> str | bool | None: | ||
return self.value.resolve(context) | ||
|
||
@classmethod | ||
def from_bit(cls, bit: str) -> Param: | ||
if "=" in bit: | ||
name, raw_value = bit.split("=", 1) | ||
value = Value(raw_value.strip()) | ||
else: | ||
name, value = bit, Value(True) | ||
return cls(name, value) | ||
return ( | ||
self.value.resolve(context) if isinstance(self.value, Value) else self.value | ||
) | ||
|
||
|
||
@dataclass | ||
class Value: | ||
raw: str | bool | None | ||
|
||
def resolve(self, context: Context | dict[str, Any]) -> Any: | ||
match (self.raw, self.is_quoted): | ||
case (None, _): | ||
return None | ||
|
||
case (str(raw_str), False) if raw_str == "False": | ||
return None | ||
case (str(raw_str), False) if raw_str == "True": | ||
return True | ||
|
||
case (bool(b), _): | ||
return b if b else None | ||
|
||
case (str(raw_str), False): | ||
try: | ||
return template.Variable(raw_str).resolve(context) | ||
except template.VariableDoesNotExist: | ||
return raw_str | ||
|
||
case (_, True): | ||
return str(self.raw)[1:-1] | ||
|
||
@property | ||
def is_quoted(self) -> bool: | ||
if self.raw is None or isinstance(self.raw, bool): | ||
return False | ||
|
||
return self.raw.startswith(("'", '"')) and self.raw.endswith(self.raw[0]) | ||
raw: FilterExpression | ||
|
||
def resolve(self, context: Context | dict[str, Any], is_attr=False) -> Any: | ||
if is_attr and self.raw.token == "False": | ||
return None | ||
if self.raw.is_var: | ||
try: | ||
self.raw.var.resolve(context) | ||
except VariableDoesNotExist: | ||
return self.raw.token | ||
return self.raw.resolve(context) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ | |||||||||||||||||||||||||
from django.template.base import Token | ||||||||||||||||||||||||||
from django.template.context import Context | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
from django_bird._typing import RawTagBits | ||||||||||||||||||||||||||
from django_bird._typing import TagBits | ||||||||||||||||||||||||||
from django_bird._typing import override | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
@@ -23,7 +24,7 @@ def do_bird(parser: Parser, token: Token) -> BirdNode: | |||||||||||||||||||||||||
raise template.TemplateSyntaxError(msg) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
name = bits.pop(0) | ||||||||||||||||||||||||||
attrs: TagBits = [] | ||||||||||||||||||||||||||
attrs: TagBits = {} | ||||||||||||||||||||||||||
isolated_context = False | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
for bit in bits: | ||||||||||||||||||||||||||
|
@@ -33,13 +34,18 @@ def do_bird(parser: Parser, token: Token) -> BirdNode: | |||||||||||||||||||||||||
case "/": | ||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||
case _: | ||||||||||||||||||||||||||
attrs.append(bit) | ||||||||||||||||||||||||||
if "=" in bit: | ||||||||||||||||||||||||||
key, value = bit.split("=") | ||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||
key = bit | ||||||||||||||||||||||||||
value = "True" | ||||||||||||||||||||||||||
attrs[key] = parser.compile_filter(value) | ||||||||||||||||||||||||||
Comment on lines
+37
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to make sure to only split on the first
Suggested change
I think I'm guilty of doing the same thing elsewhere in the library, so I need to go through myself and make sure this edge case is handled correctly as well in any of those spots. |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
nodelist = parse_nodelist(bits, parser) | ||||||||||||||||||||||||||
return BirdNode(name, attrs, nodelist, isolated_context) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
def parse_nodelist(bits: TagBits, parser: Parser) -> NodeList | None: | ||||||||||||||||||||||||||
def parse_nodelist(bits: RawTagBits, parser: Parser) -> NodeList | None: | ||||||||||||||||||||||||||
# self-closing tag | ||||||||||||||||||||||||||
# {% bird name / %} | ||||||||||||||||||||||||||
if len(bits) > 0 and bits[-1] == "/": | ||||||||||||||||||||||||||
|
@@ -55,7 +61,7 @@ class BirdNode(template.Node): | |||||||||||||||||||||||||
def __init__( | ||||||||||||||||||||||||||
self, | ||||||||||||||||||||||||||
name: str, | ||||||||||||||||||||||||||
attrs: TagBits, | ||||||||||||||||||||||||||
attrs: dict[str, str], | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type hint needs adjustment slightly:
Suggested change
|
||||||||||||||||||||||||||
nodelist: NodeList | None, | ||||||||||||||||||||||||||
isolated_context: bool = False, | ||||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
from typing import final | ||
|
||
from django import template | ||
from django.template.base import FilterExpression | ||
from django.template.base import Parser | ||
from django.template.base import Token | ||
from django.template.context import Context | ||
|
@@ -14,7 +15,7 @@ | |
TAG = "bird:prop" | ||
|
||
|
||
def do_prop(_parser: Parser, token: Token) -> PropNode: | ||
def do_prop(parser: Parser, token: Token) -> PropNode: | ||
_tag, *bits = token.split_contents() | ||
if not bits: | ||
msg = f"{TAG} tag requires at least one argument" | ||
|
@@ -26,14 +27,14 @@ def do_prop(_parser: Parser, token: Token) -> PropNode: | |
name, default = prop.split("=") | ||
except ValueError: | ||
name = prop | ||
default = None | ||
default = "None" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder what the consequence of passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess |
||
|
||
return PropNode(name, default, bits) | ||
return PropNode(name, parser.compile_filter(default), bits) | ||
|
||
|
||
@final | ||
class PropNode(template.Node): | ||
def __init__(self, name: str, default: str | None, attrs: TagBits): | ||
def __init__(self, name: str, default: FilterExpression, attrs: TagBits): | ||
self.name = name | ||
self.default = default | ||
self.attrs = attrs | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I don't mind this, but I wonder if we could do a bit better to gain a bit more clarity about the relationship between these two types? E.g.: