Migrating to 0.6¶
Version 0.6 no longer reads reactive values stored inside other values, such as a signal held by a signal or a list of signals; your code reads them explicitly. It also changes how equality, effects, and errors work. After upgrading, check both your results and how they update when inputs change.
Prepare on 0.5.1 first¶
You can make these changes before upgrading:
- Enable migration warnings while exercising representative workflows.
- Read
.valuewhen a function should return a reactive object's current value. - Call
deep_unrefinside callbacks that need values from nested containers. - Register custom containers with
deep_unref.register(Type). - Replace
rx.peek(fn)withrx.tap(fn). - Make changes through the
Signalthat owns the value.
Binding, batch(), and untracked() require 0.6. Version 0.5.1 still reads
nested values for you and keeps the old equality and effect timing rules, so
test those behaviors again after upgrading, even if no warnings appear.
Following versus storing¶
Replace Signal(source) with Binding(source) when the outer object should
follow another signal's value:
from signified import Binding, Signal
source = Signal(1)
# Before 0.6: selected = Signal(source)
selected = Binding(source)
source.value = 2
assert selected.value == 2
In 0.6, Signal(source) stores the source object itself. Likewise,
Computed(lambda: source) returns that object. Change it to
Computed(lambda: source.value) to calculate from its current value.
unref and Binding read one reactive object at a time. If that object's value
is another reactive object, it is returned as-is. For more on replacing sources,
see Binding.
Container arguments and serialization¶
Decorated functions previously resolved reactive values inside container arguments for you. Move that work inside the function:
from signified import Signal, computed, deep_unref
values = [Signal(1), Signal(2)]
# Before 0.6: total = computed(sum)(values)
total = computed(lambda items: sum(deep_unref(items)))(values)
assert total.value == 3
values[0].value = 10
assert total.value == 12
Direct reactive arguments are still read automatically. deep_unref remains
supported, with the handler API introduced in 0.5.1. Calling it before creating
the computation does not make that computation follow the original inputs.
Unregistered iterables now pass through unchanged. Register your exact custom type if you need its contents resolved. Colliding keys or set members now raise an error instead of silently losing data. See Nested values for registration, JSON, supported containers, and copying limits.
Equality and mutation¶
Review code that relied on equal lists, dictionaries, or arrays suppressing updates. These now compare by object identity, so a new object counts as a change even if its contents are equal. Exact built-in scalar values still compare by value; see the full equality rules.
Effects can now skip running when their calculated inputs return unchanged results, including when a binding switches to an equal-valued source. If an effect must react to the original signal's changes, read that signal as well.
Only Signal forwards attribute and item writes. Change the owning signal
instead of writing through a Computed or Binding. Unknown attribute names
now raise AttributeError. Raw object changes still need an explicit
.update(); see Lists and dictionaries.
Batch writes and untracked reads¶
Review code that assumes effects run once per write, in a fixed order, or again after every failed read:
batch()defers effects until its outermost block exits, including the first run of an effect created inside the block. Writes are still immediate.- One effect can cause further runs by changing another value. Batching does not undo writes on error or provide thread safety.
- A
Computednow saves an ordinary exception until an input changes or.invalidate()is called. Repeated reads raise the same saved error without rerunning the function. New failed evaluations and recovery count as changes. - Other pending effects continue after an ordinary effect failure. Multiple
failures are raised together as an
ExceptionGroup; useexcept*to handle them when needed.
See How updates work for lifetime, error recovery, and combined batch errors.
rx.peek(fn) is removed. Use rx.tap(fn) to inspect a calculation when it runs,
or untracked() to read without adding a dependency. tap saves its result
and skips unread intermediate values; it is not an automatic effect.
Optional migration diagnostics¶
Warnings are available in 0.5.1 and 0.6, and removed in 0.7. Run them on one of those versions before upgrading further:
from signified import migration
migration.enable_warnings()
# Or wrap the code being checked in: with migration.warnings():
You can also set SIGNIFIED_MIGRATION_WARNINGS=1 before import. To make warnings
fail your tests:
Warnings flag reactive values stored in signals, returned from computations,
or passed inside containers to decorated functions. Checking does not read
reactive values or consume iterators, and a Binding following a reactive
source does not warn.
Warnings can miss reactive values hidden inside your own objects, and they also flag signals you store on purpose. They say nothing about equality, effect timing, or forwarded writes, so review those separately. Turn warnings off once you are done.