Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exclude removed reactables #1750

Merged
merged 8 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750

### Changed

Expand All @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Removed

- Methods `MessagePump.emit` and `MessagePump.emit_no_wait` https://github.com/Textualize/textual/pull/1738
- Removed `reactive.watch` in favor of DOMNode.watch.

## [0.10.1] - 2023-01-20

Expand Down
4 changes: 2 additions & 2 deletions src/textual/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Horizontal
from textual.reactive import reactive, watch
from textual.reactive import reactive
from textual.widgets import (
Button,
Checkbox,
Expand Down Expand Up @@ -203,7 +203,7 @@ def compose(self) -> ComposeResult:
yield Static("Dark mode toggle", classes="label")

def on_mount(self) -> None:
watch(self.app, "dark", self.on_dark_change, init=False)
self.watch(self.app, "dark", self.on_dark_change, init=False)

def on_dark_change(self, dark: bool) -> None:
self.query_one(Checkbox).value = self.app.dark
Expand Down
20 changes: 19 additions & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from ._context import NoActiveAppError
from ._node_list import NodeList
from ._types import CallbackType
from .binding import Bindings, BindingType
from .color import BLACK, WHITE, Color
from .css._error_tools import friendly_list
Expand All @@ -31,7 +32,7 @@
from .css.styles import RenderStyles, Styles
from .css.tokenize import IDENTIFIER
from .message_pump import MessagePump
from .reactive import Reactive
from .reactive import Reactive, _watch
from .timer import Timer
from .walk import walk_breadth_first, walk_depth_first

Expand Down Expand Up @@ -647,6 +648,23 @@ def displayed_children(self) -> list[Widget]:
"""
return [child for child in self.children if child.display]

def watch(
self,
obj: DOMNode,
attribute_name: str,
callback: CallbackType,
init: bool = True,
) -> None:
"""Watches for modifications to reactive attributes on another object.

Args:
obj: Object containing attribute to watch.
attribute_name: Attribute to watch.
callback: A callback to run when attribute changes.
init: Check watchers on first call.
"""
_watch(self, obj, attribute_name, callback, init=init)

def get_pseudo_classes(self) -> Iterable[str]:
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.

Expand Down
13 changes: 13 additions & 0 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ def log(self) -> Logger:
"""
return self.app._logger

@property
def is_attached(self) -> bool:
"""Is the node is attached to the app via the DOM."""
from .app import App

node = self

while not isinstance(node, App):
if node._parent is None:
return False
node = node._parent
return True

def _attach(self, parent: MessagePump) -> None:
"""Set the parent, and therefore attach this node to the tree.

Expand Down
27 changes: 19 additions & 8 deletions src/textual/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from . import events
from ._callback import count_parameters
from ._types import MessageTarget
from ._types import MessageTarget, CallbackType

if TYPE_CHECKING:
from .dom import DOMNode
Expand Down Expand Up @@ -242,9 +242,18 @@ def invoke_watcher(
if callable(watch_function):
invoke_watcher(watch_function, old_value, value)

watchers: list[Callable] = getattr(obj, "__watchers", {}).get(name, [])
for watcher in watchers:
invoke_watcher(watcher, old_value, value)
# Process "global" watchers
watchers: list[tuple[Reactable, Callable]]
watchers = getattr(obj, "__watchers", {}).get(name, [])
# Remove any watchers for reactables that have since closed
if watchers:
watchers[:] = [
(reactable, callback)
for reactable, callback in watchers
if reactable.is_attached and not reactable._closing
]
for _, callback in watchers:
invoke_watcher(callback, old_value, value)

@classmethod
def _compute(cls, obj: Reactable) -> None:
Expand Down Expand Up @@ -316,10 +325,12 @@ def __init__(
)


def watch(
def _watch(
node: DOMNode,
obj: Reactable,
attribute_name: str,
callback: Callable[[Any], object],
callback: CallbackType,
*,
init: bool = True,
) -> None:
"""Watch a reactive variable on an object.
Expand All @@ -333,11 +344,11 @@ def watch(

if not hasattr(obj, "__watchers"):
setattr(obj, "__watchers", {})
watchers: dict[str, list[Callable]] = getattr(obj, "__watchers")
watchers: dict[str, list[tuple[Reactable, Callable]]] = getattr(obj, "__watchers")
watcher_list = watchers.setdefault(attribute_name, [])
if callback in watcher_list:
return
watcher_list.append(callback)
watcher_list.append((node, callback))
if init:
current_value = getattr(obj, attribute_name, None)
Reactive._check_watchers(obj, attribute_name, current_value)
4 changes: 2 additions & 2 deletions src/textual/widgets/_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from rich.text import Text

from .. import events
from ..reactive import Reactive, watch
from ..reactive import Reactive
from ..widget import Widget


Expand Down Expand Up @@ -66,7 +66,7 @@ async def watch_highlight_key(self, value) -> None:
self.refresh()

def on_mount(self) -> None:
watch(self.screen, "focused", self._focus_changed)
self.watch(self.screen, "focused", self._focus_changed)

def _focus_changed(self, focused: Widget | None) -> None:
self._key_text = None
Expand Down
6 changes: 3 additions & 3 deletions src/textual/widgets/_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from rich.text import Text

from ..widget import Widget
from ..reactive import Reactive, watch
from ..reactive import Reactive


class HeaderIcon(Widget):
Expand Down Expand Up @@ -133,5 +133,5 @@ def set_title(title: str) -> None:
def set_sub_title(sub_title: str) -> None:
self.query_one(HeaderTitle).sub_text = sub_title

watch(self.app, "title", set_title)
watch(self.app, "sub_title", set_sub_title)
self.watch(self.app, "title", set_title)
self.watch(self.app, "sub_title", set_sub_title)
158 changes: 158 additions & 0 deletions tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -14124,6 +14124,164 @@

'''
# ---
# name: test_screen_switch
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>

@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}

.terminal-1316892474-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}

.terminal-1316892474-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}

.terminal-1316892474-r1 { fill: #c5c8c6 }
.terminal-1316892474-r2 { fill: #e3e3e3 }
.terminal-1316892474-r3 { fill: #e1e1e1 }
.terminal-1316892474-r4 { fill: #dde8f3;font-weight: bold }
.terminal-1316892474-r5 { fill: #ddedf9 }
</style>

<defs>
<clipPath id="terminal-1316892474-clip-terminal">
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
</clipPath>
<clipPath id="terminal-1316892474-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1316892474-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
</defs>

<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1316892474-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">ModalApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>

<g transform="translate(9, 41)" clip-path="url(#terminal-1316892474-clip-terminal)">
<rect fill="#282828" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="12.2" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="24.4" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="85.4" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="97.6" y="1.5" width="329.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="427" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="524.6" y="1.5" width="329.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="854" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="866.2" y="1.5" width="0" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="866.2" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#282828" x="963.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="12.2" y="25.9" width="963.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="0" y="562.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="36.6" y="562.7" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="219.6" y="562.7" width="756.4" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-1316892474-matrix">
<text class="terminal-1316892474-r2" x="12.2" y="20" textLength="12.2" clip-path="url(#terminal-1316892474-line-0)">⭘</text><text class="terminal-1316892474-r2" x="427" y="20" textLength="97.6" clip-path="url(#terminal-1316892474-line-0)">ModalApp</text><text class="terminal-1316892474-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1316892474-line-0)">
</text><text class="terminal-1316892474-r3" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-1)">B</text><text class="terminal-1316892474-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-1)">
</text><text class="terminal-1316892474-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-2)">
</text><text class="terminal-1316892474-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-3)">
</text><text class="terminal-1316892474-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-4)">
</text><text class="terminal-1316892474-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1316892474-line-5)">
</text><text class="terminal-1316892474-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-6)">
</text><text class="terminal-1316892474-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-7)">
</text><text class="terminal-1316892474-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-8)">
</text><text class="terminal-1316892474-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-9)">
</text><text class="terminal-1316892474-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1316892474-line-10)">
</text><text class="terminal-1316892474-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-11)">
</text><text class="terminal-1316892474-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-12)">
</text><text class="terminal-1316892474-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-13)">
</text><text class="terminal-1316892474-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-14)">
</text><text class="terminal-1316892474-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1316892474-line-15)">
</text><text class="terminal-1316892474-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-16)">
</text><text class="terminal-1316892474-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-17)">
</text><text class="terminal-1316892474-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1316892474-line-18)">
</text><text class="terminal-1316892474-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1316892474-line-19)">
</text><text class="terminal-1316892474-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1316892474-line-20)">
</text><text class="terminal-1316892474-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1316892474-line-21)">
</text><text class="terminal-1316892474-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1316892474-line-22)">
</text><text class="terminal-1316892474-r4" x="0" y="581.2" textLength="36.6" clip-path="url(#terminal-1316892474-line-23)">&#160;A&#160;</text><text class="terminal-1316892474-r5" x="36.6" y="581.2" textLength="183" clip-path="url(#terminal-1316892474-line-23)">&#160;Push&#160;screen&#160;A&#160;</text>
</g>
</g>
</svg>

'''
# ---
# name: test_textlog_max_lines
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
Expand Down
38 changes: 38 additions & 0 deletions tests/snapshot_tests/snapshot_apps/screen_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Header, Footer


class ScreenA(Screen):
BINDINGS = [("b", "switch_to_b", "Switch to screen B")]

def compose(self) -> ComposeResult:
yield Header()
yield Static("A")
yield Footer()

def action_switch_to_b(self):
self.app.switch_screen(ScreenB())


class ScreenB(Screen):
def compose(self) -> ComposeResult:
yield Header()
yield Static("B")
yield Footer()


class ModalApp(App):
BINDINGS = [("a", "push_a", "Push screen A")]

def compose(self) -> ComposeResult:
yield Header()
yield Footer()

def action_push_a(self) -> None:
self.push_screen(ScreenA())


if __name__ == "__main__":
app = ModalApp()
app.run()
Loading