-
-
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?
Conversation
Promise I haven't forgotten about this. I've been down the Rust/LSP rabbit hole working on https://github.com/joshuadavidthomas/django-language-server with any spare time I have. Sorry for just letting this sit here! Thanks for taking the time to work on it (and for the nice write up in your blog post)! |
No worries, I know what it's like! 🧡 |
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.
Great work @benbacardi! Truly sorry for the enormous delay in putting together a proper review. I started it a few times, but couldn't push it over the edge to finish.
Left a few comments -- some nits, some questions, a couple small blockers -- nothing major here.
Before merge, we'll need some additional test cases with actual filters in the args for a bird
tag as well as some documentation updates. I'm fine taking the docs on myself, though you're welcome a crack at it if you're up for it.
I realize given the time gulf between when you submitted this and my review now, you may have lost interest in finishing it which is totally understandable. I'll leave this open for you to finish if you would like, and if not I'll take it over in a week or two, make some gut decisions around my suggestions and merge it while crediting you.
Thanks again for the contribution and for your patience!
RawTagBits = list[str] | ||
TagBits = dict[str, FilterExpression] |
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.:
RawTagBits = list[str] | |
TagBits = dict[str, FilterExpression] | |
RawTagBits = list[str] | |
ParsedTagBits = dict[str, FilterExpression] |
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 comment
The 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 TypeAlias
approach, I wonder if that could that could plug this leak up a bit.
@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.
if isinstance(self.value, Value): | ||
value = self.value.resolve(context, is_attr=True) | ||
else: | ||
value = self.value |
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.
If the tagged union approach is taken, this could just be reverted.
if "=" in bit: | ||
key, value = bit.split("=") | ||
else: | ||
key = bit | ||
value = "True" | ||
attrs[key] = parser.compile_filter(value) |
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.
Need to make sure to only split on the first =
, in case the bit
has more than one e.g. {% bird button data-foo="bar=qux" %}
-- a contrived example, but you get the idea.
if "=" in bit: | |
key, value = bit.split("=") | |
else: | |
key = bit | |
value = "True" | |
attrs[key] = parser.compile_filter(value) | |
if "=" in bit: | |
key, value = bit.split("=", 1) | |
else: | |
key = bit | |
value = "True" | |
attrs[key] = parser.compile_filter(value) |
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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Type hint needs adjustment slightly:
attrs: dict[str, str], | |
attrs: dict[str, FilterExpression], |
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what the consequence of passing None
vs "None"
through as a FilterExpression
? A quick glance at django.template.base.FilterExpression
, I'm not totally sure. Need some tests to verify behavior is consistent and backwards compatible.
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.
I guess None
would raise a VariableDoesNotExist
exception, right?
This is a draft PR for #181. It's a bit of a reshuffle for parameters, in particular the
Value
class, whereraw
is now expected to be aFilterExpression
(fromdjango.template.base
).This means we can now call components as such, and the Django filters are correctly parsed:
A summary of the changes:
bird
tag (anda bird:prop
tag) are parsed into key/value pairs split on=
, where the value is aFilterExpression
instanceFilterExpression
instance is passed directly to theValue
class when parsing theParams
from the nodeFilterExpression
is rendered with the context to provide the final value, and this greatly simplifies theValue.render
methoddisabled=False
disappear from theattrs
rather than appearing asdisabled="False"
—i.e. to mimic current behaviour.There is one side effect that I'm not sure whether it's a problem or not. Currently, a prop passed with an in-template value of
False
ends up with a value ofNone
inside the component, but with the above changes it will come through as it was originally withFalse
.My biggest bugbear with it is having to test the
VariableDoesNotExist
before calling the filter's resolve function, because the function itself has no way to stop it swallowing the error and returning the default empty string. I'd rather not do this, but it would mean no longer supporting unquoted raw strings. Personally I think that'd be an improvement, though…I'm only considering the PR a draft because it clearly needs your input, @joshuadavidthomas — and I'm also not sure how best to rewrite all the
test_params.py
tests that don't expectFilterExpression
instances all over the place! I've rejiggle a couple of the other tests (but locally the asset tests are also failing for seemingly unrelated reasons, so I'm not sure what will happen when they're run under CI).All thoughts appreciated! I realise this isn't a "one-line change" and therefore a trivial PR to review/approve/reject.