From b19b909a4577dbf1f74deaa00c4e268d3e2b8398 Mon Sep 17 00:00:00 2001 From: Darsh Joshi Date: Fri, 23 Oct 2026 14:23:40 -0400 Subject: [PATCH 1/8] refactor: build the backend var format error in a Field helper Field.__format__ now raises an error built by Field._no_frontend_var_error, so other code paths that reject a Field can reuse the same message. --- packages/reflex-base/src/reflex_base/vars/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 500579629ce..dfe1aaacf83 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -4340,6 +4340,14 @@ def __format__(self, format_spec: str) -> str: Raises: BackendVarFormatError: Always; the field has no frontend var. """ + raise self._no_frontend_var_error() + + def _no_frontend_var_error(self) -> BackendVarFormatError: + """Build the error for using the field where a frontend var is needed. + + Returns: + The error naming the field and how to use it in the UI instead. + """ path = f"{self._owner.__name__}.{self._name}" if self._owner else None name = f"'{path}'" if path else repr(self) if self._backend: @@ -4358,7 +4366,7 @@ def __format__(self, format_spec: str) -> str: ) else: msg = f"{name} has no frontend var, so it cannot be used in the UI." - raise BackendVarFormatError(msg) + return BackendVarFormatError(msg) def _get_raw(self, instance: Any) -> FIELD_TYPE | None: """Get the value on a state instance, never wrapped in a proxy. From b078b6194236dd4348a98e6d66338e67ff4f6b9e Mon Sep 17 00:00:00 2001 From: Darsh Joshi Date: Fri, 23 Oct 2026 14:27:24 -0400 Subject: [PATCH 2/8] fix: raise BackendVarFormatError for a backend var in a LiteralVar A backend var (or a mixin field) passed where a Var is built from a Python value, such as rx.Var.create([State._secret]), rx.toast(State._secret) or rx.image(src=State._secret), reached the generic fallthrough of LiteralVar.create and _get_all_var_data_without_creating_var_dispatch and failed with TypeError: Unsupported type ... for LiteralVar. Both now raise the same BackendVarFormatError that formatting the field raises. --- .../reflex-base/src/reflex_base/vars/base.py | 8 ++++ tests/units/components/test_component.py | 27 ++++++++++++++ tests/units/reflex_base/vars/test_base.py | 37 +++++++++++++++++-- tests/units/test_event.py | 17 +++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index dfe1aaacf83..d044ed74a28 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -1870,6 +1870,7 @@ def _create_literal_var( The var. Raises: + BackendVarFormatError: If the value is a field with no frontend var. TypeError: If the value is not a supported type for LiteralVar. """ if (literal_subclass := _literal_var_by_type.get(type(value))) is not None: @@ -1928,6 +1929,9 @@ def _create_literal_var( if isinstance(value, range): return ArrayVar.range(value.start, value.stop, value.step) + if isinstance(value, Field): + raise value._no_frontend_var_error() + msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value!r}." raise TypeError(msg) @@ -1958,6 +1962,7 @@ def _get_all_var_data_without_creating_var_dispatch( The var data or None. Raises: + BackendVarFormatError: If the value is a field with no frontend var. TypeError: If the value is not a supported type for LiteralVar. """ from .object import LiteralObjectVar @@ -2007,6 +2012,9 @@ def _get_all_var_data_without_creating_var_dispatch( if isinstance(value, range): return None + if isinstance(value, Field): + raise value._no_frontend_var_error() + msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value!r}." raise TypeError(msg) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index c141365e6ed..326b406af17 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -1,6 +1,7 @@ import copy import random import re +from collections.abc import Callable from contextlib import nullcontext from dataclasses import dataclass from typing import Any, ClassVar, TypedDict @@ -22,6 +23,7 @@ ) from reflex_base.style import Style from reflex_base.utils.exceptions import ( + BackendVarFormatError, ChildrenTypeError, EventFnArgMismatchError, EventHandlerArgTypeMismatchError, @@ -806,6 +808,31 @@ class BackendPropState(BaseState): rx.heading("x", size=BackendPropState._size) # pyright: ignore[reportArgumentType] +class BackendSrcState(BaseState): + """A state with a backend var, for untyped prop misuse tests.""" + + _src: str = "a.png" + + +@pytest.mark.parametrize( + "create", + [lambda f: rx.image(src=f), lambda f: rx.data_table(data=f)], + ids=["image_src", "data_table_data"], +) +def test_component_render_backend_var_prop_raises_format_error( + create: Callable[[Any], Component], +): + """A backend var passed to an untyped prop raises the backend var format error. + + Args: + create: Creates a component with the backend var as a prop. + """ + with pytest.raises( + BackendVarFormatError, match=r"Backend var 'BackendSrcState\._src' exists only" + ): + create(BackendSrcState._src).render() + + def test_component_create_backend_var_class_name(): """A backend var in a class_name list reports its repr, not a format error.""" diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 242cf2e85a9..e82b93d95dd 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -1460,16 +1460,45 @@ class Model(EvenMoreBasicBaseState): Var(_js_expr="x")[Model._secret] # pyright: ignore[reportIndexIssue] -def test_backend_field_literal_var_reports_repr(): - """Creating a LiteralVar from a backend var reports its repr, not a format error.""" +@pytest.mark.parametrize( + "use", + [ + LiteralVar.create, + lambda f: str(Var.create([f])), + lambda f: str(Var.create({"a": f})), + lambda f: Var.create([f])._get_all_var_data(), + lambda f: Var.create({"a": f})._get_all_var_data(), + ], + ids=["literal", "list", "dict", "list_var_data", "dict_var_data"], +) +def test_backend_field_literal_var_raises_format_error( + use: typing.Callable[[Any], Any], +): + """A backend var inside a LiteralVar raises the backend var format error. + + Args: + use: Turns the backend field into a LiteralVar, or reads its var data. + """ class Model(EvenMoreBasicBaseState): _secret: int = 42 with pytest.raises( - TypeError, match=r"Tried to create a LiteralVar from Field\(default=42" + BackendVarFormatError, match=r"Backend var 'Model\._secret' exists only" + ): + use(Model._secret) + + +def test_mixin_field_literal_var_raises_format_error(): + """A mixin's field inside a LiteralVar says to use the including state.""" + + class Mixin(EvenMoreBasicBaseState, mixin=True): + count: int = 0 + + with pytest.raises( + BackendVarFormatError, match=r"Var 'Mixin\.count' is declared on a mixin state" ): - LiteralVar.create(Model._secret) + str(Var.create([Mixin.count])) def test_mistyped_backend_field_value_logs_repr(caplog: pytest.LogCaptureFixture): diff --git a/tests/units/test_event.py b/tests/units/test_event.py index abc07d9a963..873b711c2ff 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -28,6 +28,7 @@ from reflex_base.registry import RegistrationContext from reflex_base.utils import format, log from reflex_base.utils.exceptions import ( + BackendVarFormatError, EventHandlerArgTypeMismatchError, EventHandlerTypeError, EventHandlerValueError, @@ -312,6 +313,22 @@ def test_backend_var_event_trigger_reports_repr(value: Any, match: str): EventChain.create(value=value, args_spec=lambda: ()) +@pytest.mark.parametrize( + "action", [rx.toast, rx.console_log], ids=["toast", "console_log"] +) +def test_backend_var_event_action_raises_format_error(action: Callable[[Any], Any]): + """A backend var passed to an event action raises the backend var format error. + + Args: + action: The event action given the backend var. + """ + with pytest.raises( + BackendVarFormatError, + match=r"Backend var 'BackendTriggerState\._secret' exists only", + ): + str(Var.create(action(BackendTriggerState._secret))) + + def test_backend_var_upload_progress_reports_repr(): """A backend var passed as on_upload_progress reports its repr.""" upload = rx.upload_files( From 046bcaa8ad7bad9475bd99991b1b74b50c85a176 Mon Sep 17 00:00:00 2001 From: Darsh Joshi Date: Fri, 23 Oct 2026 14:27:24 -0400 Subject: [PATCH 3/8] docs: add news fragment for backend vars in LiteralVar --- packages/reflex-base/news/+backend-var-literal-var.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/+backend-var-literal-var.bugfix.md diff --git a/packages/reflex-base/news/+backend-var-literal-var.bugfix.md b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md new file mode 100644 index 00000000000..742e0103160 --- /dev/null +++ b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md @@ -0,0 +1 @@ +Passing a backend var where a Var is built from a Python value, e.g. `rx.Var.create([State._secret])`, `rx.toast(State._secret)` or `rx.image(src=State._secret)`, now raises `BackendVarFormatError` with the same explanation as `f"{State._secret}"`, instead of a generic `TypeError: Unsupported type ... for LiteralVar`. From 4e71e5e08e536b81d814dd682eb520edd3a7bed2 Mon Sep 17 00:00:00 2001 From: Farhan Date: Wed, 4 Nov 2026 22:10:01 +0500 Subject: [PATCH 4/8] refactor: share the unsupported-literal error and update its docs Build the LiteralVar fallthrough error in one helper, so both dispatchers keep their previous complexity. Update the BackendVarFormatError docstring, which is now raised outside string formatting, and the 0.10 upgrade guide, which still quoted the old TypeError for a backend var used as a prop. --- docs/changelog/upgrading/upgrading-to-0-10.md | 2 +- .../src/reflex_base/utils/exceptions.py | 2 +- .../reflex-base/src/reflex_base/vars/base.py | 27 ++++++++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/changelog/upgrading/upgrading-to-0-10.md b/docs/changelog/upgrading/upgrading-to-0-10.md index d6f26a7f8d1..a8b880e289e 100644 --- a/docs/changelog/upgrading/upgrading-to-0-10.md +++ b/docs/changelog/upgrading/upgrading-to-0-10.md @@ -9,7 +9,7 @@ This page covers the 0.10 changes most likely to break an existing app, or to be ## Reading a backend var on a state class -Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child) or `TypeError: Unsupported type for LiteralVar` (as a prop), and interpolating it without a conversion in an f-string or with `str.format()` raises `BackendVarFormatError` (`!r`, `!s` and `str()` embed the `Field` text instead). Reading the var on a state instance, such as `self._items`, is unchanged. +Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child) or `BackendVarFormatError` (as a prop; earlier 0.10 releases raise `TypeError: Unsupported type for LiteralVar`), and interpolating it without a conversion in an f-string or with `str.format()` raises `BackendVarFormatError` (`!r`, `!s` and `str()` embed the `Field` text instead). Reading the var on a state instance, such as `self._items`, is unchanged. Replace the class read with what the code meant: diff --git a/packages/reflex-base/src/reflex_base/utils/exceptions.py b/packages/reflex-base/src/reflex_base/utils/exceptions.py index 46d5fb863ea..12df4a2fb18 100644 --- a/packages/reflex-base/src/reflex_base/utils/exceptions.py +++ b/packages/reflex-base/src/reflex_base/utils/exceptions.py @@ -77,7 +77,7 @@ class VarTypeError(ReflexError, TypeError): class BackendVarFormatError(VarTypeError): - """Raised when a field with no frontend var, such as a backend var, is formatted as a string.""" + """Raised when a field with no frontend var, such as a backend var, is used where a frontend var is needed.""" class VarValueError(ReflexError, ValueError): diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index d044ed74a28..414f2b82c91 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -1799,6 +1799,21 @@ def create( ) +def _unsupported_literal_error(value: Any) -> TypeError: + """Build the error for a value that no LiteralVar supports. + + Args: + value: The unsupported value. + + Returns: + The backend var error for a field with no frontend var, else a TypeError. + """ + if isinstance(value, Field): + return value._no_frontend_var_error() + msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value!r}." + return TypeError(msg) + + class LiteralVar(Var[VAR_TYPE]): """Base class for immutable literal vars.""" @@ -1929,11 +1944,7 @@ def _create_literal_var( if isinstance(value, range): return ArrayVar.range(value.start, value.stop, value.step) - if isinstance(value, Field): - raise value._no_frontend_var_error() - - msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value!r}." - raise TypeError(msg) + raise _unsupported_literal_error(value) if not TYPE_CHECKING: create = _create_literal_var @@ -2012,11 +2023,7 @@ def _get_all_var_data_without_creating_var_dispatch( if isinstance(value, range): return None - if isinstance(value, Field): - raise value._no_frontend_var_error() - - msg = f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value!r}." - raise TypeError(msg) + raise _unsupported_literal_error(value) @property def _var_value(self) -> Any: From 3b8f0263dbdc6d9876c4443d09e67ae5f5606a4a Mon Sep 17 00:00:00 2001 From: Farhan Date: Wed, 4 Nov 2026 22:43:20 +0500 Subject: [PATCH 5/8] fix: raise BackendVarFormatError for backend vars outside LiteralVar A backend var given as a component id, as on_upload_progress, or as page title, description or image failed with an unrelated TypeError, ValueError or ChildrenTypeError that did not name the var. These now raise BackendVarFormatError. Page metadata is checked in add_page, so the error points at the user's call. A state var's field read from get_fields() no longer claims to have no frontend var; the message says to use the var instead. Add a test for get_upload_url with a backend var. --- news/+backend-var-page-meta.bugfix.md | 1 + .../news/+backend-var-literal-var.bugfix.md | 2 +- .../src/reflex_base/components/component.py | 6 +++++ .../src/reflex_base/event/__init__.py | 5 +++- .../reflex-base/src/reflex_base/vars/base.py | 7 +++++- reflex/app.py | 17 +++++++++++++ tests/units/components/core/test_upload.py | 18 +++++++++++++- tests/units/components/test_component.py | 8 +++++-- tests/units/reflex_base/vars/test_base.py | 13 ++++++++++ tests/units/test_app.py | 24 +++++++++++++++++++ tests/units/test_event.py | 9 ++++--- 11 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 news/+backend-var-page-meta.bugfix.md diff --git a/news/+backend-var-page-meta.bugfix.md b/news/+backend-var-page-meta.bugfix.md new file mode 100644 index 00000000000..5e3520a7ea1 --- /dev/null +++ b/news/+backend-var-page-meta.bugfix.md @@ -0,0 +1 @@ +`app.add_page(...)` now raises `BackendVarFormatError` when `title`, `description` or `image` is a backend var such as `State._title`, instead of failing later at compile time with an unrelated error. diff --git a/packages/reflex-base/news/+backend-var-literal-var.bugfix.md b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md index 742e0103160..e4e923549d4 100644 --- a/packages/reflex-base/news/+backend-var-literal-var.bugfix.md +++ b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md @@ -1 +1 @@ -Passing a backend var where a Var is built from a Python value, e.g. `rx.Var.create([State._secret])`, `rx.toast(State._secret)` or `rx.image(src=State._secret)`, now raises `BackendVarFormatError` with the same explanation as `f"{State._secret}"`, instead of a generic `TypeError: Unsupported type ... for LiteralVar`. +Passing a backend var where a Var is built from a Python value, e.g. `rx.Var.create([State._secret])`, `rx.toast(State._secret)`, `rx.image(src=State._secret)`, `rx.box(id=State._secret)` or `rx.upload_files(on_upload_progress=State._secret)`, now raises `BackendVarFormatError` with the same explanation as `f"{State._secret}"`, instead of a generic `TypeError` or `ValueError`. Using a state var's field from `State.get_fields()` in the UI now says to use the var instead of claiming it has no frontend var. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index c9097c51a4d..293525966f6 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -42,6 +42,7 @@ from reflex_base.vars.base import ( _PY_OR_IMPORT, CachedVarOperation, + Field, LiteralNoneVar, LiteralVar, Var, @@ -2102,10 +2103,15 @@ def get_ref(self) -> str | None: Returns: The ref name. + + Raises: + BackendVarFormatError: If the id is a field with no frontend var. """ # do not create a ref if the id is dynamic or unspecified if self.id is None or isinstance(self.id, Var): return None + if isinstance(self.id, Field): + raise self.id._no_frontend_var_error() return format.format_ref(self.id) def _get_all_refs(self) -> dict[str, None]: diff --git a/packages/reflex-base/src/reflex_base/event/__init__.py b/packages/reflex-base/src/reflex_base/event/__init__.py index 7f6c6ffe43e..caa9b9f893a 100644 --- a/packages/reflex-base/src/reflex_base/event/__init__.py +++ b/packages/reflex-base/src/reflex_base/event/__init__.py @@ -57,7 +57,7 @@ value_inside_optional, ) from reflex_base.vars import VarData -from reflex_base.vars.base import LiteralVar, Var, _owner_state +from reflex_base.vars.base import Field, LiteralVar, Var, _owner_state from reflex_base.vars.function import ( ENCODE_URI_COMPONENT, ArgsFunctionOperation, @@ -1388,6 +1388,7 @@ def _as_event_spec( The upload EventSpec. Raises: + BackendVarFormatError: If the on_upload_progress is a field with no frontend var. ValueError: If the on_upload_progress is not a valid event handler. """ from reflex_components_core.core.upload import ( @@ -1443,6 +1444,8 @@ def _as_event_spec( events = call_event_fn( on_upload_progress, self.on_upload_progress_args_spec ) + elif isinstance(on_upload_progress, Field): + raise on_upload_progress._no_frontend_var_error() else: msg = f"{on_upload_progress!s} is not a valid event handler." raise ValueError(msg) diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 414f2b82c91..4e3bea8fa7c 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -4365,7 +4365,12 @@ def _no_frontend_var_error(self) -> BackendVarFormatError: """ path = f"{self._owner.__name__}.{self._name}" if self._owner else None name = f"'{path}'" if path else repr(self) - if self._backend: + if self._var is not None: + msg = ( + f"{name} is the field of a state var, not the var. Use {path}" + " to use it in the UI." + ) + elif self._backend: msg = ( f"Backend var {name} exists only on the server and has no" " frontend value, so it cannot be used in the UI. Use" diff --git a/reflex/app.py b/reflex/app.py index 97d7d7472e5..2723f212009 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -60,6 +60,7 @@ Send, is_non_string_sequence, ) +from reflex_base.vars.base import Field from reflex_base.vars.dep_tracking import is_dependency from reflex_components_core.base.error_boundary import ErrorBoundary from reflex_components_core.base.fragment import Fragment @@ -286,6 +287,20 @@ def default_error_boundary(*children: Component, **props) -> Component: ) +def _reject_page_meta_fields(*values: Any) -> None: + """Reject page metadata given as a field with no frontend var. + + Args: + *values: The page metadata values. + + Raises: + BackendVarFormatError: If a value is a field with no frontend var. + """ + for value in values: + if isinstance(value, Field): + raise value._no_frontend_var_error() + + @dataclasses.dataclass( frozen=True, ) @@ -1010,6 +1025,7 @@ def add_page( context: Values passed to page for custom page-specific logic. Raises: + BackendVarFormatError: When title, description or image is a field with no frontend var. PageValueError: When the component is not set for a non-404 page. RouteValueError: When the specified route name already exists. ValueError: When the route syntax is invalid. @@ -1062,6 +1078,7 @@ def _prepare_page( RouteValueError: When a route cannot be derived. ValueError: When the route syntax is invalid. """ + _reject_page_meta_fields(title, description, image) route = self._page_route_key(component, route) if route is None: msg = "Route must be set if component is not a callable." diff --git a/tests/units/components/core/test_upload.py b/tests/units/components/core/test_upload.py index 1fd26e09135..6431c87e0e5 100644 --- a/tests/units/components/core/test_upload.py +++ b/tests/units/components/core/test_upload.py @@ -6,6 +6,7 @@ import pytest from reflex_base.event import EventChain, EventHandler, EventSpec, parse_args_spec +from reflex_base.utils.exceptions import BackendVarFormatError from reflex_base.vars import VarData from reflex_base.vars.base import LiteralVar, Var from reflex_components_core.core._upload import ( @@ -33,7 +34,7 @@ import reflex as rx from reflex import event -from reflex.state import State +from reflex.state import BaseState, State class UploadStateTest(State): @@ -101,6 +102,21 @@ def test_get_upload_url(): assert isinstance(url, Var) +class BackendUploadUrlState(BaseState): + """A state with a backend var, for get_upload_url misuse tests.""" + + _file: str = "a.txt" + + +def test_get_upload_url_backend_var_raises_format_error(): + """A backend var passed as the file path raises the backend var format error.""" + with pytest.raises( + BackendVarFormatError, + match=r"Backend var 'BackendUploadUrlState\._file' exists only", + ): + get_upload_url(BackendUploadUrlState._file) # pyright: ignore[reportArgumentType] + + def test__on_drop_spec(): assert isinstance(_on_drop_spec(LiteralVar.create([])), tuple) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 326b406af17..0567ff461e3 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -816,8 +816,12 @@ class BackendSrcState(BaseState): @pytest.mark.parametrize( "create", - [lambda f: rx.image(src=f), lambda f: rx.data_table(data=f)], - ids=["image_src", "data_table_data"], + [ + lambda f: rx.image(src=f), + lambda f: rx.data_table(data=f), + lambda f: rx.box(id=f), + ], + ids=["image_src", "data_table_data", "box_id"], ) def test_component_render_backend_var_prop_raises_format_error( create: Callable[[Any], Component], diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index e82b93d95dd..1f8d9513d98 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -1441,6 +1441,19 @@ class Mixin(EvenMoreBasicBaseState, mixin=True): f"{Mixin.count}" +def test_state_var_field_format_raises(): + """A state var's field, read from get_fields(), says to use the var instead.""" + + class Model(BaseState): + count: int = 0 + + with pytest.raises( + BackendVarFormatError, + match=r"^'Model\.count' is the field of a state var.* Use Model\.count ", + ): + f"{Model.get_fields()['count']}" + + def test_unbound_field_format_raises(): """An unbound field has no Var to format, and the error shows its definition.""" with pytest.raises( diff --git a/tests/units/test_app.py b/tests/units/test_app.py index ae575628094..fc230a4819c 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -381,6 +381,30 @@ def test_prepare_404_page_empty_string_metadata_uses_defaults(): assert prepared.page.description == constants.Page404.DESCRIPTION +class BackendMetaState(BaseState): + """A state with a backend var, for page metadata misuse tests.""" + + _meta: str = "x" + + +@pytest.mark.parametrize("key", ["title", "description", "image"]) +def test_add_page_backend_var_meta_raises_format_error( + app: App, index_page: ComponentCallable, key: str +): + """A backend var passed as page metadata raises when the page is added. + + Args: + app: The app to test. + index_page: The index page. + key: The page metadata argument given the backend var. + """ + with pytest.raises( + exceptions.BackendVarFormatError, + match=r"Backend var 'BackendMetaState\._meta' exists only", + ): + app.add_page(index_page, route="/meta", **{key: BackendMetaState._meta}) # pyright: ignore[reportArgumentType] + + def test_add_page_set_route(app: App, index_page: ComponentCallable): """Test adding a page to an app. diff --git a/tests/units/test_event.py b/tests/units/test_event.py index 873b711c2ff..1d065ae447b 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -329,13 +329,16 @@ def test_backend_var_event_action_raises_format_error(action: Callable[[Any], An str(Var.create(action(BackendTriggerState._secret))) -def test_backend_var_upload_progress_reports_repr(): - """A backend var passed as on_upload_progress reports its repr.""" +def test_backend_var_upload_progress_raises_format_error(): + """A backend var passed as on_upload_progress raises the backend var format error.""" upload = rx.upload_files( upload_id="u", on_upload_progress=BackendTriggerState._secret, # pyright: ignore[reportArgumentType] ) - with pytest.raises(ValueError, match=r"^Field\(default=42.* is not a valid"): + with pytest.raises( + BackendVarFormatError, + match=r"Backend var 'BackendTriggerState\._secret' exists only", + ): upload.as_event_spec(handler=cast(EventHandler, BackendTriggerState.on_upload)) From 11bb76e2fc51931ecc821d587c9ff88890e182f1 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza <62690310+FarhanAliRaza@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:37:04 +0500 Subject: [PATCH 6/8] Update docs/changelog/upgrading/upgrading-to-0-10.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- docs/changelog/upgrading/upgrading-to-0-10.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog/upgrading/upgrading-to-0-10.md b/docs/changelog/upgrading/upgrading-to-0-10.md index a8b880e289e..16789a3d325 100644 --- a/docs/changelog/upgrading/upgrading-to-0-10.md +++ b/docs/changelog/upgrading/upgrading-to-0-10.md @@ -9,7 +9,7 @@ This page covers the 0.10 changes most likely to break an existing app, or to be ## Reading a backend var on a state class -Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child) or `BackendVarFormatError` (as a prop; earlier 0.10 releases raise `TypeError: Unsupported type for LiteralVar`), and interpolating it without a conversion in an f-string or with `str.format()` raises `BackendVarFormatError` (`!r`, `!s` and `str()` embed the `Field` text instead). Reading the var on a state instance, such as `self._items`, is unchanged. +Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child); a prop may raise `BackendVarFormatError` when its value is built or rendered (earlier 0.10 releases may instead raise `TypeError: Unsupported type for LiteralVar`), while typed props can still reject it with `TypeError` during component creation. Interpolating it without a conversion in an f-string or with `str.format()` raises `BackendVarFormatError` (`!r`, `!s` and `str()` embed the `Field` text instead). Reading the var on a state instance, such as `self._items`, is unchanged. Replace the class read with what the code meant: From 979fbc4888b14acfc4468c4f147d210c53c6f4de Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 5 Nov 2026 01:16:15 +0500 Subject: [PATCH 7/8] fix: raise BackendVarFormatError when a backend var is converted with str() Field.__str__ now raises the same error as Field.__format__, so str(F), "%s" % F and f"{F!s}" no longer put the Field repr in the page. The error sites that used !s to keep their own error (class_name lists, untyped Var item access, Button variant/size, event triggers) now raise BackendVarFormatError, which names the var and the fix. repr() and !r still give the Field text. --- docs/changelog/upgrading/upgrading-to-0-10.md | 2 +- .../news/+backend-var-literal-var.bugfix.md | 2 +- .../reflex-base/src/reflex_base/vars/base.py | 8 ++++++ tests/units/components/test_component.py | 7 +++-- tests/units/reflex_base/vars/test_base.py | 26 ++++++++++++++----- .../components/base/test_button.py | 18 +++++-------- tests/units/test_event.py | 19 ++++++++------ 7 files changed, 52 insertions(+), 30 deletions(-) diff --git a/docs/changelog/upgrading/upgrading-to-0-10.md b/docs/changelog/upgrading/upgrading-to-0-10.md index 16789a3d325..d2aff4c1c6c 100644 --- a/docs/changelog/upgrading/upgrading-to-0-10.md +++ b/docs/changelog/upgrading/upgrading-to-0-10.md @@ -9,7 +9,7 @@ This page covers the 0.10 changes most likely to break an existing app, or to be ## Reading a backend var on a state class -Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child); a prop may raise `BackendVarFormatError` when its value is built or rendered (earlier 0.10 releases may instead raise `TypeError: Unsupported type for LiteralVar`), while typed props can still reject it with `TypeError` during component creation. Interpolating it without a conversion in an f-string or with `str.format()` raises `BackendVarFormatError` (`!r`, `!s` and `str()` embed the `Field` text instead). Reading the var on a state instance, such as `self._items`, is unchanged. +Reading a backend var on the state class, such as `State._items`, now returns its `Field` instead of the var's default value ([#7312](https://github.com/reflex-dev/reflex/pull/7312)). Passing it to a component raises `ChildrenTypeError` (as a child); a prop may raise `BackendVarFormatError` when its value is built or rendered (earlier 0.10 releases may instead raise `TypeError: Unsupported type for LiteralVar`), while typed props can still reject it with `TypeError` during component creation. Turning it into text with an f-string, `str.format()`, `str()` or `%` raises `BackendVarFormatError` (earlier 0.10 releases embed the `Field` text for `!s`, `str()` and `%`; `!r` and `repr()` still do). Reading the var on a state instance, such as `self._items`, is unchanged. Replace the class read with what the code meant: diff --git a/packages/reflex-base/news/+backend-var-literal-var.bugfix.md b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md index e4e923549d4..7c8c8951eb0 100644 --- a/packages/reflex-base/news/+backend-var-literal-var.bugfix.md +++ b/packages/reflex-base/news/+backend-var-literal-var.bugfix.md @@ -1 +1 @@ -Passing a backend var where a Var is built from a Python value, e.g. `rx.Var.create([State._secret])`, `rx.toast(State._secret)`, `rx.image(src=State._secret)`, `rx.box(id=State._secret)` or `rx.upload_files(on_upload_progress=State._secret)`, now raises `BackendVarFormatError` with the same explanation as `f"{State._secret}"`, instead of a generic `TypeError` or `ValueError`. Using a state var's field from `State.get_fields()` in the UI now says to use the var instead of claiming it has no frontend var. +Passing a backend var where a Var is built from a Python value, e.g. `rx.Var.create([State._secret])`, `rx.toast(State._secret)`, `rx.image(src=State._secret)`, `rx.box(id=State._secret)` or `rx.upload_files(on_upload_progress=State._secret)`, now raises `BackendVarFormatError` with the same explanation as `f"{State._secret}"`, instead of a generic `TypeError` or `ValueError`. Converting a backend var to text with `str()`, `%` or `!s` now raises it too, instead of putting the `Field` text in the page. Using a state var's field from `State.get_fields()` in the UI now says to use the var instead of claiming it has no frontend var. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 0d1c55109a3..f78099572c0 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -4359,6 +4359,14 @@ def __format__(self, format_spec: str) -> str: """ raise self._no_frontend_var_error() + def __str__(self) -> str: + """Refuse to convert the field to text, as ``__format__`` does. + + Raises: + BackendVarFormatError: Always; the field has no frontend var. + """ + raise self._no_frontend_var_error() + def _no_frontend_var_error(self) -> BackendVarFormatError: """Build the error for using the field where a frontend var is needed. diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 0567ff461e3..30c25936ace 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -838,12 +838,15 @@ def test_component_render_backend_var_prop_raises_format_error( def test_component_create_backend_var_class_name(): - """A backend var in a class_name list reports its repr, not a format error.""" + """A backend var in a class_name list raises the backend var format error.""" class BackendClassNameState(BaseState): _cls: str = "a" - with pytest.raises(TypeError, match=r"got value Field\(default='a'"): + with pytest.raises( + BackendVarFormatError, + match=r"Backend var 'BackendClassNameState\._cls' exists only", + ): rx.box(class_name=["b", BackendClassNameState._cls]) diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index a14ef5f091a..508cb5186c7 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -32,7 +32,6 @@ BackendVarFormatError, ReflexRuntimeError, StateValueError, - UntypedVarError, ) from reflex_base.utils.imports import ImportVar from reflex_base.utils.types import get_field_type @@ -1411,12 +1410,23 @@ class Model(EvenMoreBasicBaseState): assert model._value == 1 +@pytest.mark.parametrize( + "to_text", + [ + lambda f: f"{f}px", + lambda f: f"{f!s}px", + lambda f: str(f) + "px", + lambda f: "%spx" % f, # noqa: UP031 + ], + ids=["format", "format_s", "str", "percent"], +) @pytest.mark.parametrize("name", ["_secret", "bookkeeping"]) -def test_backend_field_format_raises(name: str): - """Formatting a backend var raises instead of embedding its repr. +def test_backend_field_format_raises(name: str, to_text: typing.Callable[[Any], str]): + """Turning a backend var into text raises instead of embedding its repr. Args: name: The backend field to format, underscore-prefixed or is_var=False. + to_text: Turns the field into text. """ class Model(EvenMoreBasicBaseState): @@ -1426,7 +1436,7 @@ class Model(EvenMoreBasicBaseState): with pytest.raises( BackendVarFormatError, match=rf"Backend var 'Model\.{name}' exists only" ): - f"{getattr(Model, name)}px" + to_text(getattr(Model, name)) @pytest.mark.parametrize("mixin", [False, True]) @@ -1490,13 +1500,15 @@ def test_unbound_field_format_raises(): f"{field(default=0)}" -def test_untyped_var_item_access_reports_backend_var_key(): - """Indexing an untyped Var with a backend var names the key, not a format error.""" +def test_untyped_var_item_access_backend_var_key_raises_format_error(): + """Indexing an untyped Var with a backend var raises the backend var format error.""" class Model(EvenMoreBasicBaseState): _secret: int = 42 - with pytest.raises(UntypedVarError, match=r"access the item 'Field\(default=42"): + with pytest.raises( + BackendVarFormatError, match=r"Backend var 'Model\._secret' exists only" + ): Var(_js_expr="x")[Model._secret] # pyright: ignore[reportIndexIssue] diff --git a/tests/units/reflex_components_internal/components/base/test_button.py b/tests/units/reflex_components_internal/components/base/test_button.py index 5a210f89738..e62ac2ff3d0 100644 --- a/tests/units/reflex_components_internal/components/base/test_button.py +++ b/tests/units/reflex_components_internal/components/base/test_button.py @@ -1,6 +1,7 @@ """Tests for the internal button component.""" import pytest +from reflex_base.utils.exceptions import BackendVarFormatError from reflex_components_internal.components.base.button import Button from reflex.state import BaseState @@ -12,19 +13,14 @@ class ButtonState(BaseState): _secret: str = "x" -@pytest.mark.parametrize( - ("validate", "match"), - [ - (Button.validate_variant, r"Invalid variant: Field\(default='x'"), - (Button.validate_size, r"Invalid size: Field\(default='x'"), - ], -) -def test_validate_backend_var_reports_repr(validate, match: str): - """A backend var passed as a variant or size reports its repr. +@pytest.mark.parametrize("validate", [Button.validate_variant, Button.validate_size]) +def test_validate_backend_var_raises_format_error(validate): + """A backend var passed as a variant or size raises the backend var format error. Args: validate: The validator under test. - match: The expected error message pattern. """ - with pytest.raises(ValueError, match=match): + with pytest.raises( + BackendVarFormatError, match=r"Backend var 'ButtonState\._secret' exists only" + ): validate(ButtonState._secret) diff --git a/tests/units/test_event.py b/tests/units/test_event.py index e870869acb3..8e1cb0ca9cc 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -295,21 +295,24 @@ def on_upload(self, files: list[rx.UploadFile]): @pytest.mark.parametrize( - ("value", "match"), + "value", [ - (BackendTriggerState._secret, r"Invalid event chain: Field\(default=42"), - ([BackendTriggerState._secret], r"Invalid event: Field\(default=42"), - (lambda: BackendTriggerState._secret, r"-> Field\(default=42"), + BackendTriggerState._secret, + [BackendTriggerState._secret], + lambda: BackendTriggerState._secret, ], + ids=["chain", "list", "lambda"], ) -def test_backend_var_event_trigger_reports_repr(value: Any, match: str): - """Binding a backend var as an event trigger reports its repr, not a format error. +def test_backend_var_event_trigger_raises_format_error(value: Any): + """Binding a backend var as an event trigger raises the backend var format error. Args: value: The misused event trigger value. - match: The expected error message pattern. """ - with pytest.raises(ValueError, match=match): + with pytest.raises( + BackendVarFormatError, + match=r"Backend var 'BackendTriggerState\._secret' exists only", + ): EventChain.create(value=value, args_spec=lambda: ()) From 224db98cc75c6eb0d08344fe6906e33666b7c9d9 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 5 Nov 2026 01:17:32 +0500 Subject: [PATCH 8/8] test: cover backend vars passed to the experimental hook wrappers --- tests/units/experimental/test_hooks.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/units/experimental/test_hooks.py diff --git a/tests/units/experimental/test_hooks.py b/tests/units/experimental/test_hooks.py new file mode 100644 index 00000000000..6c70eb6567b --- /dev/null +++ b/tests/units/experimental/test_hooks.py @@ -0,0 +1,39 @@ +"""Tests for the experimental React hook wrappers.""" + +from collections.abc import Callable +from typing import Any + +import pytest +from reflex_base.utils.exceptions import BackendVarFormatError + +from reflex.experimental.hooks import const, useCallback, useContext, useRef, useState +from reflex.state import BaseState + + +class HooksState(BaseState): + """A state with a backend var.""" + + _secret: str = "x" + + +@pytest.mark.parametrize( + "hook", + [ + lambda f: const("value", f), + lambda f: useCallback(f, []), + useContext, + useRef, + lambda f: useState("value", f), + ], + ids=["const", "useCallback", "useContext", "useRef", "useState"], +) +def test_hook_backend_var_raises_format_error(hook: Callable[[Any], Any]): + """A backend var passed to a hook raises instead of putting its repr in JS. + + Args: + hook: Calls a hook with the backend var. + """ + with pytest.raises( + BackendVarFormatError, match=r"Backend var 'HooksState\._secret' exists only" + ): + hook(HooksState._secret)