Add Field.set_default and point the state var assignment error at the declaring state
Follow-up to https://github.com/reflex-dev/reflex/pull/7516. It addresses @FarhanAliRaza's two post-merge review comments on the TypeError raised when a state var is assigned through its class.
Type of change
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update
Changes
Field.set_default(default=MISSING, *, default_factory=None) (comment). This is one supported way to change a state var's default. It takes exactly one of default (positional or keyword) or default_factory (keyword-only), and raises TypeError if it gets neither or both. As in Field and _default_arguments, a None factory means no factory.
- An immutable
defaultis stored as is. - A mutable
defaultbecomes a factory that returns a copy of the value as it was when set (it goes through_default_arguments, whichField._with_defaultalso uses). - A
default_factoryreplaces anydefaultthat is set.
Either way, default and default_factory stay mutually exclusive, so a stale default can no longer silently win over a new factory. Sessions never share a mutable default.
_default_arguments now copies a mutable value once up front. Instances read the default lazily, so without that copy, a caller who changed their list after set_default would change the default too. The same protection now applies to State.add_var, and a mutable class-declared default is deep-copied once when its class is created. Mutability is checked with types.is_immutable on the value itself only, not on items nested inside a tuple.
WatchlistState.__fields__["ticker"].set_default("MSFT")
WatchlistState.__fields__["symbols"].set_default(["AAPL", "MSFT"])
WatchlistState.__fields__["opened_at"].set_default(default_factory=time.time)
The error names the state that declares the var (comment). The message now names the field's _owner and points at the owner's field. When you assign through a substate, it also says the change applies to every state that inherits the var. Because set_default handles every kind of value, the suggestion is the same for all of them:
'count' is a state var of Parent, inherited by Child; assigning it on Child would replace the var.
Set its default with Parent.__fields__['count'].set_default(...), which applies to every state that
inherits it, or declare class-level config as ClassVar.
del Child.count (and the cleanup of a refused mock.patch.object) gets the same attribution. A state that mixes in a mixin's var owns its own copy of the field, so the error names that state, not the mixin. A substate of it names it as the declaring state. Only an assignment on the mixin class itself names the mixin.
Docs and tests now use set_default instead of assigning .default or .default_factory:
- Docs: Changing Defaults, ComponentState, and the 0.10 upgrade guide.
- Tests: ComponentState unit and integration tests, and the storage hydration test.
The upgrade guide also notes a 0.9 trap. On 0.9, assigning a mutable .default was safe because each instance got a copy. In 0.10 every instance shares the field's default, so a mutable value assigned there leaks changes between sessions.
Tests
test_field_set_defaultcovers:- an immutable default, passed positionally and by keyword;
- a mutable default replacing a set
defaultwith a copying factory, with no sharing between instances or with the caller; - an immutable default replacing a factory;
- a
default_factoryreplacing a default and being called for each instance; Nonestill working as adefault.
test_field_set_default_takes_exactly_onecovers the neither, explicitdefault_factory=None, and both cases.- The error tests (
test_class_assignment_error_names_the_declaring_state,..._names_the_state_mixing_in_a_var,..._suggests_set_default) check the owner attribution and the uniformset_default(...)suggestion. uv run pytest tests/unitspasses excepttests/units/reflex_cliandtests/units/reflex_bench/drivers/test_browser.py, which fail the same way onmainin my environment.tests/integration/tests_playwright/test_hydration_storage.pypasses (32 tests).- I couldn't run the Selenium
tests/integration/test_component_state.pylocally, because the container can't download a ChromeDriver. ruff check,ruff format, anduv run pyright reflex testsare clean.
š¤ Generated with Claude Code