diff --git a/docs/changelog/upgrading/upgrading-to-0-10.md b/docs/changelog/upgrading/upgrading-to-0-10.md index d6f26a7f8d1..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) 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); 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/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 new file mode 100644 index 00000000000..7c8c8951eb0 --- /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)`, `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/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 caba63db14d..ac1e091912f 100644 --- a/packages/reflex-base/src/reflex_base/event/__init__.py +++ b/packages/reflex-base/src/reflex_base/event/__init__.py @@ -58,7 +58,14 @@ value_inside_optional, ) from reflex_base.vars import VarData -from reflex_base.vars.base import LiteralVar, ToOperation, Var, _operands, _owner_state +from reflex_base.vars.base import ( + Field, + LiteralVar, + ToOperation, + Var, + _operands, + _owner_state, +) from reflex_base.vars.function import ( ENCODE_URI_COMPONENT, ArgsFunctionOperation, @@ -1507,6 +1514,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 ( @@ -1562,6 +1570,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/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 e2357971030..f78099572c0 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -1801,6 +1801,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.""" @@ -1872,6 +1887,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: @@ -1930,8 +1946,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 @@ -1960,6 +1975,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 @@ -2009,8 +2025,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: @@ -4342,9 +4357,30 @@ def __format__(self, format_spec: str) -> str: Raises: BackendVarFormatError: Always; the field has no frontend var. """ + 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. + + 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" @@ -4360,7 +4396,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. diff --git a/reflex/app.py b/reflex/app.py index 7094d726091..eeca899907c 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 @@ -288,6 +289,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, ) @@ -1169,6 +1184,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. @@ -1221,6 +1237,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 c141365e6ed..30c25936ace 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,13 +808,45 @@ 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.""" + """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/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) diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index c9045d71638..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]) @@ -1468,6 +1478,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( @@ -1477,26 +1500,57 @@ 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] -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/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_app.py b/tests/units/test_app.py index 0a6b626e506..4a98e84c85b 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -387,6 +387,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 49a32697f03..8e1cb0ca9cc 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, @@ -294,31 +295,53 @@ 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: ()) -def test_backend_var_upload_progress_reports_repr(): - """A backend var passed as on_upload_progress reports its repr.""" +@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_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))