feat: Add expression serializer for AttributeGenerator - #54
gazpachoking merged 9 commits into
Conversation
| return BaseAttr( | ||
| "attr", | ||
| value=javascript(_as_javascript_expressions(attrs)), | ||
| alias=self._alias, | ||
| ) |
There was a problem hiding this comment.
I believe _as_javascript_expressions(attrs) is not needed for attr and class_ since we don't expect it to be recursive and this line could be done with a dictionary comprehension instead. e.g.
return BaseAttr(
"attr",
value=javascript({key: JSExpression(value) for key, value in attrs.items()}),
alias=self._alias,
)But I left it with _as_javascript_expressions since it felt cleaner this way.
c503f64 to
081be82
Compare
|
Thanks for working on this! One thing I've been uncomfortable with since I wrote it is the global flag to turn everything from a string to js expressions at once. I'm wondering if we tackle that at the same time to simplify this. Rather than the _SENTINEL_START = "⟪JS:"
_SENTINEL_END = ":JS⟫"
class JSExpr(str):
def __new__(cls, expr: str):
return super().__new__(
cls,
f"{_SENTINEL_START}{expr}{_SENTINEL_END}",
)
def dumps_js(value) -> str:
result = json.dumps(value, separators=(",", ":"))
return re.sub(
rf'"{re.escape(_SENTINEL_START)}(.*?){re.escape(_SENTINEL_END)}"',
lambda m: json.loads(f'"{m.group(1)}"'), # to unescape quotes and stuff when the dump turned it into a string
result,
)If we wanted backwards compat for the global way, we could have it just recurse the object wrapping every str with a JSExpr when What do you think, is that a meaningful simplification? Worth it to be able to have mixed strings and expressions? |
|
I like this |
0dca7ee to
b2c428c
Compare
|
Well, the current implementation already supports this, just updated the type checking support to allow for this explicitly. And added a test to confirm: assert ds.signals(
literal="window.innerWidth",
expression=JSExpression("window.innerWidth"),
) == {"data-signals": ('{"literal": "window.innerWidth", "expression": (window.innerWidth)}')} |
|
Cool, the user api is the same either way then. I just thought the regex implementation was a bit shorter to read and more performant. I'm fine with your way if you prefer it. The performance difference was something like 2-5x better with the sentinel way, but that really doesn't matter much when we won't realistically be calling it that many times per render, and the baseline time is really small. |
|
I like this approach only because we don't have to escape the sentinel values in an edge case where someone wants to display the sentinel values by storing that literal string in a signal. Maybe I'm overly cautious there :) I agree about performance not mattering here. If someone is interested in performance they can construct the string manually and we should focus on ergonomics here. If you are okay with it, then this PR is ready to merge! |
This PR extracts the expression serializer parts of the draft PR from #13
Instead of the existing
_js_object, in this PR a recursive serializer is used. Specifically, the previous_js_objecthelper recursively handled dictionaries, but inserted other values using their Python string representation.This PR introduces a
javascript()helper which:True,False, andNoneas JavaScript;For example, if a user were to write this code:
Before this PR:
After this PR:
In this example, previously the signal would be
["window.innerWidth", "window.innerHeight"]which would be the string text but with this PR the signals will be[window.innerWidth, window.innerHeight], i.e. the value at the time.