Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/changelog/upgrading/upgrading-to-0-10.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class 'reflex_base.vars.base.Field'> 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); a prop may raise `BackendVarFormatError` when its value is built or rendered (earlier 0.10 releases may instead raise `TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> 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:

Expand Down
1 change: 1 addition & 0 deletions news/+backend-var-page-meta.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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)`, `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.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from reflex_base.vars.base import (
_PY_OR_IMPORT,
CachedVarOperation,
Field,
LiteralNoneVar,
LiteralVar,
Var,
Expand Down Expand Up @@ -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]:
Expand Down
5 changes: 4 additions & 1 deletion packages/reflex-base/src/reflex_base/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/reflex-base/src/reflex_base/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
40 changes: 34 additions & 6 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -1870,6 +1885,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:
Expand Down Expand Up @@ -1928,8 +1944,7 @@ def _create_literal_var(
if isinstance(value, range):
return ArrayVar.range(value.start, value.stop, value.step)

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
Expand Down Expand Up @@ -1958,6 +1973,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
Expand Down Expand Up @@ -2007,8 +2023,7 @@ def _get_all_var_data_without_creating_var_dispatch(
if isinstance(value, range):
return None

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:
Expand Down Expand Up @@ -4340,9 +4355,22 @@ 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:
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"
Expand All @@ -4358,7 +4386,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.
Expand Down
17 changes: 17 additions & 0 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."
Expand Down
18 changes: 17 additions & 1 deletion tests/units/components/core/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
31 changes: 31 additions & 0 deletions tests/units/components/test_component.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,6 +23,7 @@
)
from reflex_base.style import Style
from reflex_base.utils.exceptions import (
BackendVarFormatError,
ChildrenTypeError,
EventFnArgMismatchError,
EventHandlerArgTypeMismatchError,
Expand Down Expand Up @@ -806,6 +808,35 @@ 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),
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],
):
"""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."""

Expand Down
50 changes: 46 additions & 4 deletions tests/units/reflex_base/vars/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -1460,16 +1473,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):
Expand Down
24 changes: 24 additions & 0 deletions tests/units/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading