Repository navigation
feat(vars): add get_leaf_vars - #7606
Conversation
A var operation such as `State.items.filter(...)` renders to a fresh value every time it is evaluated, so a React hook reading one cannot list it as a dependency without recomputing on every render. `get_leaf_vars` takes a var apart into the leaf vars it is built from: a `CustomVarOperation` yields its operands and a cast yields the var it wraps, recursively, deduplicated in order of first appearance. Any other var, a literal or function included, is a leaf and comes back alone. It is the one place to extend when other containers need unwrapping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
[Medium impact] No new blocking issue was established in the changes since the previous review. SummaryAdds
Reviews (6) · Last reviewed commit: "feat(vars): keep a function whole when a..." · Reviewed by Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Merging this PR will not alter performance
|
`get_leaf_vars` only unwrapped `CustomVarOperation` and casts, so an array slice, an object item, a string concatenation or a function call stayed a leaf and a hook depending on it recomputed on every render. It also kept a literal container whole, so a memo prop or loop var inside `[[value]]` was invisible, and kept a lambda whole, so one closing over a loop var could not be depended on without listing the whole function. Unwrap every cached operation through the vars held in its fields, with `CustomVarOperation` and casts handled as before. Dissolve a literal: a scalar reads nothing, a tuple, list or mapping reads the vars it holds, and any other value such as an event, a color or a component stays whole, since what it reads is not known. Read a function through its body minus the names its parameters bind, so a predicate closing over nothing yields no leaf and one closing over a loop var yields that var. A test pins how every cached operation class is sorted, so a new one has to be placed on purpose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…whole `get_leaf_vars` skipped a raw value held by an operation or a literal container, although the renderer passes it through `LiteralVar.create`: an f-string with a var inside a list, or a color passed as a call argument, read a var that was not reported. Convert every raw operand the same way before taking it apart, so those reads are found. A color literal now reads its parts instead of staying whole, and ranges and datetimes count as constants, since they render the same every time. A literal that stays whole inside a lambda body, such as an event, may read the lambda's parameters without its expression equalling a parameter name, so hoisting it out of the function could reference a name that only exists inside the callback. Such a function now stays whole as a leaf. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
`test_get_leaf_vars_sorts_every_cached_operation` failed on Python 3.11 and 3.12: a `slots=True` dataclass is a rebuilt copy of the class first defined, and the discarded original stays in `__subclasses__` until it is garbage collected, so the set held two objects per class and set equality failed. Compare qualified names instead, which collapse the duplicates on every version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
The leaf tests compared rendered strings, which pass even when a leaf is a fresh object missing its var data. Assert object identity instead: every leaf is the var the compound var was built from. The one case that constructs a new object, a var that is the whole of a formatted string, is checked with `Var.equals`, which compares the var data too. For that contract to hold, a cast around a leaf now comes back as the cast the caller holds, such as `State.x` or `Var(...).to(int)`, rather than the object inside it. A cast around an operation is still seen through to the operation's operands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
…ameter
A body leaf that is not a parameter itself may still read one: a var built
from a formatted parameter such as `Var(f"{item}.length")`, or a nested
function closing over the outer parameter. Hoisting it out of the function
named something bound only inside, which crashed a hook with a
`ReferenceError`. A function now stays whole when any free leaf of its body
mentions a bound name, which also replaces the narrower check on literals
kept whole.
Fold the cast and function handling into `_operands`, so `get_leaf_vars`
only pushes operands or records a leaf and every helper stays under the
complexity limit. A cast reads what it wraps, so a cast around a function
now reads like the function instead of staying whole.
Count `Decimal` as a constant and `set` and `frozenset` as containers, type
the parameter helper's argument, and pin the operation classes defined in
`reflex` in the sorting test as well.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD
All Submissions:
Type of change
Description
Adds
reflex_base.vars.get_leaf_vars(var) -> Sequence[Var], which returns the vars a var reads, deduplicated in order of first appearance. Each leaf is the object the compound var was built from, so callers can rely on identity.A var operation such as
State.items.filter(...)orState.items[1:]renders to a fresh value every time it is evaluated, so a React hook that reads one cannot list it as a dependency without recomputing on every render. With this helper such a hook can depend onState.itemsinstead:How a var is taken apart:
CustomVarOperationreads its operands; every other cached operation (an array slice, an object item, a string concatenation, a function call) reads what its own fields hold. A raw value among them, such as a call argument or a container item, is converted throughLiteralVar.createthe way the renderer does, so an f-string with a var inside a list still reads that var.State.xcomes back asState.x. A cast around an operation or a function is seen through.Decimal, range or datetime reads nothing, a tuple, list, set or mapping reads what it holds, a color reads its parts, and any other value (an event, a component) stays whole as a leaf, since what it reads is not known.f"{item}.length"or a nested function closing overitem, since hoisting that leaf out would name something bound only inside the function.A test pins how every
CachedVarOperationsubclass inreflex_baseandreflexis sorted, so a new one has to be placed on purpose rather than falling through unnoticed.The first use is the data editor rework in #7607, which is stacked on this PR and builds the dependency arrays of its
useMemoanduseCallbackhooks from it.New Feature Submission:
Changes To Core Features:
Tests in
tests/units/reflex_base/vars/test_base.pyassert leaf identity and cover a leaf returned alone, literals dissolving (scalars, containers, nested f-strings, colors, an event kept whole), nested operations and casts with deduplication, the operations with their own fields, raw call arguments, function bodies (closures, nested lambdas, rest parameters, a color body, a cast around a function, a body leaf reading a parameter keeping the function whole), and the sorting of every cached operation class.ruff,pyrighton the touched files, andpytest tests/units/reflex_base/vars/test_base.pypass on 3.14 and in a separate 3.11 environment.🤖 Generated with Claude Code
https://claude.ai/code/session_01RqJ6TNt8AXzbDVm5kLb2nD