Skip to content

Commit 124e60c

Browse files
committed
reactive initialize
1 parent 9401cb4 commit 124e60c

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
# Unreleased
9+
10+
### Added
11+
12+
- Added `Content.simplify`
13+
- Added `textual.reactive.Initialize`
14+
815
## [5.2.0] - 2025-08-01
916

1017
### Added

src/textual/_callback.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ def count_parameters(func: Callable) -> int:
3030
param_count = _count_parameters(func) - 1
3131
else:
3232
param_count = _count_parameters(func)
33-
func._param_count = param_count
33+
try:
34+
func._param_count = param_count
35+
except TypeError:
36+
pass
3437
return param_count
3538

3639

src/textual/reactive.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ class TooManyComputesError(ReactiveError):
5656
"""Raised when an attribute has public and private compute methods."""
5757

5858

59+
class Initialize(Generic[ReactableType, ReactiveType]):
60+
"""Initialize a reactive by calling a method parent object.
61+
62+
Example:
63+
```python
64+
class InitializeApp(App):
65+
66+
def get_names(self) -> list[str]:
67+
return ["foo", "bar", "baz"]
68+
69+
# The `names` property will call `get_names` to get its default when first referenced.
70+
names = reactive(Initialize(get_names))
71+
```
72+
73+
"""
74+
75+
def __init__(self, callback: Callable[[ReactableType], ReactiveType]) -> None:
76+
self.callback = callback
77+
78+
def __call__(self, obj: ReactableType) -> ReactiveType:
79+
return self.callback(obj)
80+
81+
5982
async def await_watcher(obj: Reactable, awaitable: Awaitable[object]) -> None:
6083
"""Coroutine to await an awaitable returned from a watcher"""
6184
_rich_traceback_omit = True
@@ -118,7 +141,7 @@ class Reactive(Generic[ReactiveType]):
118141

119142
def __init__(
120143
self,
121-
default: ReactiveType | Callable[[], ReactiveType],
144+
default: ReactiveType | Callable[[], ReactiveType] | Initialize[ReactiveType],
122145
*,
123146
layout: bool = False,
124147
repaint: bool = True,
@@ -190,7 +213,11 @@ def _initialize_reactive(self, obj: Reactable, name: str) -> None:
190213
else:
191214
default_or_callable = self._default
192215
default = (
193-
default_or_callable()
216+
(
217+
default_or_callable(obj)
218+
if isinstance(default_or_callable, Initialize)
219+
else default_or_callable()
220+
)
194221
if callable(default_or_callable)
195222
else default_or_callable
196223
)
@@ -421,7 +448,7 @@ class reactive(Reactive[ReactiveType]):
421448

422449
def __init__(
423450
self,
424-
default: ReactiveType | Callable[[], ReactiveType],
451+
default: ReactiveType | Callable[[], ReactiveType] | Initialize[ReactiveType],
425452
*,
426453
layout: bool = False,
427454
repaint: bool = True,
@@ -456,7 +483,7 @@ class var(Reactive[ReactiveType]):
456483

457484
def __init__(
458485
self,
459-
default: ReactiveType | Callable[[], ReactiveType],
486+
default: ReactiveType | Callable[[], ReactiveType] | Initialize[ReactiveType],
460487
init: bool = True,
461488
always_update: bool = False,
462489
bindings: bool = False,

tests/test_reactive.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,29 @@
77
from textual.app import App, ComposeResult
88
from textual.message import Message
99
from textual.message_pump import MessagePump
10-
from textual.reactive import Reactive, TooManyComputesError, reactive, var
10+
from textual.reactive import Initialize, Reactive, TooManyComputesError, reactive, var
1111
from textual.widget import Widget
1212

1313
OLD_VALUE = 5_000
1414
NEW_VALUE = 1_000_000
1515

1616

17+
async def test_initialize():
18+
"""Test that the default accepts an Initialize instance."""
19+
20+
class InitializeApp(App):
21+
22+
def get_names(self) -> list[str]:
23+
return ["foo", "bar", "baz"]
24+
25+
names = reactive(Initialize(get_names))
26+
27+
app = InitializeApp()
28+
29+
async with app.run_test():
30+
assert app.names == ["foo", "bar", "baz"]
31+
32+
1733
async def test_watch():
1834
"""Test that changes to a watched reactive attribute happen immediately."""
1935

0 commit comments

Comments
 (0)