fix: raise BackendVarFormatError for a backend var in a LiteralVar

DA
2d ago
+95-5

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

  • Bug fix (non-breaking change which fixes an issue)

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?

Description

Class access to a field with no frontend Var (a backend var such as State._secret, an is_var=False field, or any field of a mixin state) returns the raw Field. When that Field is passed where a Var gets built from a Python value, it reaches the end of LiteralVar.create (_create_literal_var), or of its var-data twin _get_all_var_data_without_creating_var_dispatch. Neither recognises a Field, so both fall through to the generic TypeError: Unsupported type ... for LiteralVar. That covers rx.Var.create([F]), rx.Var.create({"a": F}), rx.toast(F), rx.console_log(F), rx.image(src=F) and rx.data_table(data=F) from the issue. Formatting the same field with f"{F}" already raises a clear BackendVarFormatError from Field.__format__.

This PR follows the proposal in the issue and gives the LiteralVar path the same wording:

  • Field.__format__ now builds its message in a small helper, Field._no_frontend_var_error(). The text is unchanged.
  • Both LiteralVar fallthroughs check isinstance(value, Field) right before the generic TypeError and raise that error. The check runs only on the path that was about to fail, so supported values pay nothing. BackendVarFormatError is a VarTypeError, which is a TypeError, so except TypeError code still catches it.

test_backend_field_literal_var_reports_repr asserted the old generic message, so it is replaced by tests that expect BackendVarFormatError.

Before / After (F = S._secret, S a state with _secret: str = "x"):

# before (upstream/main d28eb2e0)
rx.Var.create([F]) -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. Tried to create a LiteralVar from Field(default='x', is_var=True, annotated_type=<class 'str'>).
rx.Var.create({"a": F}) -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. ...
rx.toast(F) rendered -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. ...
rx.console_log(F) rendered -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. ...
rx.image(src=F) rendered -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. ...
rx.data_table(data=F) rendered -> builtins.TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar. ...
f"{F}" (control) -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...

# after
rx.Var.create([F]) -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server and has no frontend value, so it cannot be used in the UI. Use S._secret.default_value() for its default value, declare it as ClassVar[...] for a constant shared by all sessions, or use a regular state var for a value the UI should show and update.
rx.Var.create({"a": F}) -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...
rx.toast(F) rendered -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...
rx.console_log(F) rendered -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...
rx.image(src=F) rendered -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...
rx.data_table(data=F) rendered -> reflex_base.utils.exceptions.BackendVarFormatError: Backend var 'S._secret' exists only on the server ...

The error still comes up where the Var is built (stringified or rendered), not at the rx.toast(...) / rx.image(...) call. That is the same place as before, but now with the explanation. Not covered here, and better left as follow-ups:

  • rx.upload_files(on_upload_progress=F) and app.add_page(title=F, ...) never reach LiteralVar, so they keep their current errors.
  • The two cases in the issue comment (str(F) + "px" silently embedding the repr, and rx.box(id=F)) are also on other code paths.
  • The rx.get_upload_url(F) / reflex/experimental/hooks.py test suggestion.

Tests:

  • tests/units/reflex_base/vars/test_base.py: LiteralVar.create(F), str(Var.create([F])), str(Var.create({"a": F})), and _get_all_var_data() on the list and dict literals (this reaches the var-data twin). Plus a mixin field case for the mixin wording.
  • tests/units/test_event.py: rx.toast(F) and rx.console_log(F).
  • tests/units/components/test_component.py: rx.image(src=F) and rx.data_table(data=F) rendered.

With vars/base.py reverted to main, all 10 new cases fail with the generic TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar (8 raised at base.py:1932, 2 at :2011). With the fix they pass:

$ uv run pytest tests/units/reflex_base/vars/test_base.py tests/units/test_event.py tests/units/components/test_component.py -k format_error
14 passed, 472 deselected, 1 warning in 0.06s

$ uv run pytest tests/units --cov --no-cov-on-fail --cov-report=
Required test coverage of 72.0% reached. Total coverage: 80.37%
12243 passed, 37 skipped, 22 warnings in 134.52s (0:02:14)

ruff format, ruff check, codespell, update-pyi-files, hash-examples, unasync-reflex-build-sdk, ty, biome format and make_pyi.py --check pass. pyright --pythonplatform Linux reports 0 errors. The reflex-release changelog-check and check-headings checks pass with the news fragment packages/reflex-base/news/+backend-var-literal-var.bugfix.md.

Closes #7459