diff --git a/CHANGELOG.md b/CHANGELOG.md index ed46c3a9f2..37c99a1851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - `TextArea.line_number_start` reactive attribute https://github.com/Textualize/textual/pull/4471 +- Added `DOMNode.mutate_reactive` ### Fixed diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 8bbbc979e6..2507b14ab5 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -287,7 +287,7 @@ def _set(self, obj: Reactable, value: ReactiveType, always: bool = False) -> Non if callable(public_validate_function): value = public_validate_function(value) # If the value has changed, or this is the first time setting the value - if always or current_value != value or self._always_update: + if always or self._always_update or current_value != value: # Store the internal value setattr(obj, self.internal_name, value) diff --git a/tests/test_reactive.py b/tests/test_reactive.py index 8a22d70a13..ad2da5724d 100644 --- a/tests/test_reactive.py +++ b/tests/test_reactive.py @@ -735,7 +735,6 @@ def on_test_widget_test_message(self, event: TestWidget.TestMessage) -> None: message_senders.append(event._sender) class TestApp(App[None]): - def compose(self) -> ComposeResult: yield TestContainer() @@ -744,3 +743,43 @@ def compose(self) -> ComposeResult: pilot.app.query_one(TestWidget).make_reaction() await pilot.pause() assert message_senders == [pilot.app.query_one(TestWidget)] + + +async def test_mutate_reactive() -> None: + """Test explicitly mutating reactives""" + + watched_names: list[list[str]] = [] + + class TestWidget(Widget): + names: reactive[list[str]] = reactive(list) + + def watch_names(self, names: list[str]) -> None: + watched_names.append(names.copy()) + + class TestApp(App): + def compose(self) -> ComposeResult: + yield TestWidget() + + app = TestApp() + async with app.run_test(): + widget = app.query_one(TestWidget) + # watch method called on startup + assert watched_names == [[]] + + # Mutate the list + widget.names.append("Paul") + # No changes expected + assert watched_names == [[]] + # Explicitly mutate the reactive + widget.mutate_reactive(TestWidget.names) + # Watcher will be invoked + assert watched_names == [[], ["Paul"]] + # Make further modifications + widget.names.append("Jessica") + widget.names.remove("Paul") + # No change expected + assert watched_names == [[], ["Paul"]] + # Explicit mutation + widget.mutate_reactive(TestWidget.names) + # Watcher should be invoked + assert watched_names == [[], ["Paul"], ["Jessica"]]