Skip to content

Commit 40a205f

Browse files
authored
Merge pull request #1014 from Textualize/reactive-always-update
Reactive `always_update`
2 parents aac9d87 + 47c5a22 commit 40a205f

File tree

3 files changed

+36
-9
lines changed

3 files changed

+36
-9
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,6 @@ venv.bak/
116116

117117
# Snapshot testing report output directory
118118
tests/snapshot_tests/output
119+
120+
# Sandbox folder - convenient place for us to develop small test apps without leaving the repo
121+
sandbox/

docs/guide/reactivity.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ If you click the buttons in the above example it will show the current count. Wh
165165

166166
## Watch methods
167167

168-
Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch methods begin with `watch_` followed by the name of the attribute. If the watch method accepts a positional argument, it will be called with the new assigned value. If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value.
168+
Watch methods are another superpower.
169+
Textual will call watch methods when reactive attributes are modified.
170+
Watch methods begin with `watch_` followed by the name of the attribute.
171+
If the watch method accepts a positional argument, it will be called with the new assigned value.
172+
If the watch method accepts *two* positional arguments, it will be called with both the *old* value and the *new* value.
169173

170174
The following app will display any color you type in to the input. Try it with a valid color in Textual CSS. For example `"darkorchid"` or `"#52de44"`.
171175

@@ -192,6 +196,12 @@ The following app will display any color you type in to the input. Try it with a
192196

193197
The color is parsed in `on_input_submitted` and assigned to `self.color`. Because `color` is reactive, Textual also calls `watch_color` with the old and new values.
194198

199+
### When are watch methods called?
200+
201+
Textual only calls watch methods if the value of a reactive attribute _changes_.
202+
If the newly assigned value is the same as the previous value, the watch method is not called.
203+
You can override this behaviour by passing `always_update=True` to `reactive`.
204+
195205
## Compute methods
196206

197207
Compute methods are the final superpower offered by the `reactive` descriptor. Textual runs compute methods to calculate the value of a reactive attribute. Compute methods begin with `compute_` followed by the name of the reactive value.

src/textual/reactive.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import TYPE_CHECKING, Any, Callable, Generic, Type, TypeVar, Union
66
from weakref import WeakSet
77

8-
98
from . import events
109
from ._callback import count_parameters, invoke
1110
from ._types import MessageTarget
@@ -16,7 +15,6 @@
1615

1716
Reactable = Union[Widget, App]
1817

19-
2018
ReactiveType = TypeVar("ReactiveType")
2119

2220

@@ -37,7 +35,7 @@ class Reactive(Generic[ReactiveType]):
3735
layout (bool, optional): Perform a layout on change. Defaults to False.
3836
repaint (bool, optional): Perform a repaint on change. Defaults to True.
3937
init (bool, optional): Call watchers on initialize (post mount). Defaults to False.
40-
38+
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
4139
"""
4240

4341
def __init__(
@@ -47,11 +45,13 @@ def __init__(
4745
layout: bool = False,
4846
repaint: bool = True,
4947
init: bool = False,
48+
always_update: bool = False,
5049
) -> None:
5150
self._default = default
5251
self._layout = layout
5352
self._repaint = repaint
5453
self._init = init
54+
self._always_update = always_update
5555

5656
@classmethod
5757
def init(
@@ -60,18 +60,25 @@ def init(
6060
*,
6161
layout: bool = False,
6262
repaint: bool = True,
63+
always_update: bool = False,
6364
) -> Reactive:
6465
"""A reactive variable that calls watchers and compute on initialize (post mount).
6566
6667
Args:
6768
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
6869
layout (bool, optional): Perform a layout on change. Defaults to False.
6970
repaint (bool, optional): Perform a repaint on change. Defaults to True.
70-
71+
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
7172
Returns:
7273
Reactive: A Reactive instance which calls watchers or initialize.
7374
"""
74-
return cls(default, layout=layout, repaint=repaint, init=True)
75+
return cls(
76+
default,
77+
layout=layout,
78+
repaint=repaint,
79+
init=True,
80+
always_update=always_update,
81+
)
7582

7683
@classmethod
7784
def var(
@@ -153,7 +160,7 @@ def __set__(self, obj: Reactable, value: ReactiveType) -> None:
153160
if callable(validate_function) and not first_set:
154161
value = validate_function(value)
155162
# If the value has changed, or this is the first time setting the value
156-
if current_value != value or first_set:
163+
if current_value != value or first_set or self._always_update:
157164
# Set the first set flag to False
158165
setattr(obj, f"__first_set_{self.internal_name}", False)
159166
# Store the internal value
@@ -259,7 +266,7 @@ class reactive(Reactive[ReactiveType]):
259266
layout (bool, optional): Perform a layout on change. Defaults to False.
260267
repaint (bool, optional): Perform a repaint on change. Defaults to True.
261268
init (bool, optional): Call watchers on initialize (post mount). Defaults to True.
262-
269+
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
263270
"""
264271

265272
def __init__(
@@ -269,8 +276,15 @@ def __init__(
269276
layout: bool = False,
270277
repaint: bool = True,
271278
init: bool = True,
279+
always_update: bool = False,
272280
) -> None:
273-
super().__init__(default, layout=layout, repaint=repaint, init=init)
281+
super().__init__(
282+
default,
283+
layout=layout,
284+
repaint=repaint,
285+
init=init,
286+
always_update=always_update,
287+
)
274288

275289

276290
class var(Reactive[ReactiveType]):

0 commit comments

Comments
 (0)