diff --git a/news/6633.bugfix.md b/news/6633.bugfix.md new file mode 100644 index 00000000000..f29a7e3d0b6 --- /dev/null +++ b/news/6633.bugfix.md @@ -0,0 +1 @@ +`State.get_var_value()` no longer silently returns a wrong value when passed a Var operation — an arithmetic/concatenation expression such as `State.a + State.b`, or an indexed/item access such as `State.items[0]`. Previously it resolved the state and field of the operation's *first* operand and returned that field's value instead of the operation's result. It now raises `UnretrievableVarValueError`, consistent with how it already handled vars not associated with any state. Plain field and computed-var references continue to resolve as before. diff --git a/reflex/state.py b/reflex/state.py index e3e959a2d44..e7c96018654 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -59,6 +59,7 @@ ComputedVar, DynamicRouteVar, EvenMoreBasicBaseState, + ToOperation, Var, computed_var, dispatch, @@ -1747,8 +1748,18 @@ async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE: ) is not unset and not isinstance(var_value, Var): return var_value # pyright: ignore [reportReturnType] - var_data = var._get_all_var_data() - if var_data is None or not var_data.state: + # Unwrap any cast wrappers and resolve via the underlying var's *own* + # var data, not the recursive _get_all_var_data(). For an operation or + # derived var (e.g. State.a + State.b or State.items[0]), the recursive + # merge back-fills state/field_name from the first operand, which would + # make us silently return that operand's value instead of the operation's + # result. Only a plain field or computed var reference carries + # state + field_name on its own var data. + inner_var = var + while isinstance(inner_var, ToOperation): + inner_var = inner_var._original + var_data = inner_var._var_data + if var_data is None or not var_data.state or not var_data.field_name: msg = f"Unable to retrieve value for {var._js_expr}: not associated with any state." raise UnretrievableVarValueError(msg) # Fastish case: this var belongs to this state diff --git a/tests/units/test_state.py b/tests/units/test_state.py index f793c7af022..83aa823598a 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -4575,6 +4575,21 @@ async def test_get_var_value( "b": [4, 5, 6], } + # Regression for https://github.com/reflex-dev/reflex/issues/6629: a Var + # operation / derived var (arithmetic, indexed or item access) must not + # silently return the value of its first constituent field. Such vars have + # no retrievable value, so raise instead of returning a plausible-but-wrong one. + with pytest.raises(UnretrievableVarValueError): + await state.get_var_value(TestState.num1 + TestState.num2) + with pytest.raises(UnretrievableVarValueError): + # array[0] is a Var operation at runtime, though statically typed as the element. + await state.get_var_value(TestState.array[0]) # pyright: ignore[reportArgumentType] + with pytest.raises(UnretrievableVarValueError): + await state.get_var_value(TestState.mapping["a"]) + + # Computed vars are derived but state-bound, so they remain resolvable. + assert await state.get_var_value(TestState.sum) == pytest.approx(42 + 3.15) + @pytest.mark.asyncio async def test_get_var_value_async_computed_var(