feat(vars): add get_leaf_vars
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- New feature (non-breaking change which adds functionality)
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(...) or State.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 on State.items instead:
>>> [str(v) for v in get_leaf_vars((State.a + 1) * State.b)]
['state.a', 'state.b']
>>> [str(v) for v in get_leaf_vars(State.items.filter(lambda item: item == loop_var))]
['state.items', 'loop_var']
>>> [str(v) for v in get_leaf_vars(LiteralVar.create([[memo_prop], f"{State.name}"]))]
['memo_prop', 'state.name']
How a var is taken apart:
- A
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. - A cast reads what it wraps, so a cast around a leaf is the leaf, as the caller holds it:
State.xcomes back asState.x. A cast around an operation or a function is seen through. - A literal dissolves: a scalar,
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. - A function reads 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. It stays whole when a free leaf of its body still mentions a parameter, such as a var built from
f"{item}.length"or a nested function closing overitem, since hoisting that leaf out would name something bound only inside the function. - A state field access is a leaf.
A test pins how every CachedVarOperation subclass in reflex_base and reflex is 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 useMemo and useCallback hooks from it.
New Feature Submission:
- Does your submission pass the tests?
- Have you linted your code locally prior to submission?
Changes To Core Features:
- Have you added an explanation of what your changes do and why you'd like us to include them?
- Have you written new tests for your core changes, as applicable?
- Have you successfully ran tests with your changes locally?
Tests in tests/units/reflex_base/vars/test_base.py assert 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, pyright on the touched files, and pytest tests/units/reflex_base/vars/test_base.py pass on 3.14 and in a separate 3.11 environment.
🤖 Generated with Claude Code