Repository navigation
Add Field.set_default and point the state var assignment error at the declaring state - #7519
Conversation
…the state var assignment error Assigning a state var through a substate named the substate and suggested setting the default on its __fields__, which hides that the field belongs to the declaring state and that changing it applies to every state inheriting it. The error now names the field's owner, points at the owner's field, and says the change applies to inheriting states. For a mutable value, a plain `.default = ...` shares one object across every session, so the error suggests `default_factory = lambda: ...` instead, and tells the user to clear a set default first, since a default wins over the factory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
A state that mixes in a var owns its own copy of the field, so the error names that state, and a substate of it names it as the declaring state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
There was a problem hiding this comment.
Review completed against the latest diff
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
View guided diff | Turn on auto-fix | Re-trigger cubic
|
Merging this PR will degrade performance by 7.35%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
`Field.set_default(value)` sets a field's default in place the way `_with_default` builds one: an immutable value becomes `default` and a mutable one becomes a `default_factory` deep-copying it, so the two stay mutually exclusive and sessions never share a mutable default. `_default_arguments` now copies a mutable value once up front, so later changes by the caller do not reach the default. Since one call works for every value, the assignment error now suggests `Owner.__fields__[name].set_default(...)` uniformly. Docs, the upgrade guide and tests use set_default in place of assigning `.default` or `.default_factory`, and the upgrade guide notes that the 0.9 idiom of assigning a mutable `.default` now shares the value between sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
There was a problem hiding this comment.
All reported issues were addressed across 13 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
`set_default` now takes keyword-only `default` and `default_factory`, each defaulting to MISSING, and raises TypeError unless exactly one is given. A mutable `default` still becomes a factory returning a copy taken when it was set; a `default_factory` replaces any set default. Docs, the error message and tests use the keyword form, and the Changing Defaults docs show a factory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
The default value is the common case and the method name already says
"default", so `set_default("MSFT")` reads better than
`set_default(default="MSFT")`. `default_factory` stays keyword-only. Docs,
tests, news and the assignment error use the positional form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
Two plain conditions read better than comparing two booleans, and each case now gets an error saying what was wrong. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
FarhanAliRaza
left a comment
There was a problem hiding this comment.
I tested this PR in a small Reflex app. The app calls set_default the way a user does. It sets an immutable default, a mutable list, a default_factory, an immutable value that replaces a declared factory, and an rx.LocalStorage value. It also has two ComponentState instances that call set_default in get_component.
In the browser, the new defaults show on first load. A change to the source list after set_default does not reach the default. reset() restores the new defaults, and the factory runs again on each reset. The storage var writes to its new storage name. The two ComponentState instances keep separate defaults. A second browser session gets clean defaults, so sessions do not share the mutable default. There are no console or backend errors.
I also checked the class-assignment error. It names the state that declares the var. For an inherited var, it says which states the change affects.
set_default resets default_factory to None, not to MISSING. This is correct, because None is the field's "no factory" value. A field with MISSING in that slot fails in default_value().
I left two inline comments.
…t_arguments A field has no factory when its default_factory is None, as Field.__init__ and _default_arguments spell it, so set_default takes None for "not given" too. An explicit default_factory=None counts as not given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf
…9583012 Reconcile fragments main changed after the train materialized them: carry #7519's `set_default` wording of the #7516 breaking entries into the reflex and reflex-base a4 changelogs, and keep both materialized fragments deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SjCrbdguMkfe4bhbaREiwo
Curated from the a2-a5 passes, deduplicated and parameterized; the Python suite, pytest suites, N-004 interchange and the cse2e app were re-run on the released 0.10.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SjCrbdguMkfe4bhbaREiwo
Follow-up to #7516. It addresses @FarhanAliRaza's two post-merge review comments on the
TypeErrorraised when a state var is assigned through its class.Type of change
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 ofdefault(positional or keyword) ordefault_factory(keyword-only), and raisesTypeErrorif it gets neither or both. As inFieldand_default_arguments, aNonefactory means no factory.defaultis stored as is.defaultbecomes a factory that returns a copy of the value as it was when set (it goes through_default_arguments, whichField._with_defaultalso uses).default_factoryreplaces anydefaultthat is set.Either way,
defaultanddefault_factorystay mutually exclusive, so a staledefaultcan no longer silently win over a new factory. Sessions never share a mutable default._default_argumentsnow copies a mutable value once up front. Instances read the default lazily, so without that copy, a caller who changed their list afterset_defaultwould change the default too. The same protection now applies toState.add_var, and a mutable class-declared default is deep-copied once when its class is created. Mutability is checked withtypes.is_immutableon the value itself only, not on items nested inside a tuple.The error names the state that declares the var (comment). The message now names the field's
_ownerand 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. Becauseset_defaulthandles every kind of value, the suggestion is the same for all of them:del Child.count(and the cleanup of a refusedmock.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_defaultinstead of assigning.defaultor.default_factory:The upgrade guide also notes a 0.9 trap. On 0.9, assigning a mutable
.defaultwas safe because each instance got a copy. In 0.10 every instance shares the field'sdefault, so a mutable value assigned there leaks changes between sessions.Tests
test_field_set_defaultcovers:defaultwith a copying factory, with no sharing between instances or with the caller;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.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).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
https://claude.ai/code/session_014GtHWzR4ASUdN2JLmy6ycf