diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 82fecdf5b..648b0a4d7 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -123,6 +123,11 @@ class MessagePump(metaclass=MessagePumpMeta): """ return self.app._logger + @property + def is_attached(self) -> bool: + """Check the node is attached to the DOM""" + return self._parent is not None + def _attach(self, parent: MessagePump) -> None: """Set the parent, and therefore attach this node to the tree. diff --git a/src/textual/reactive.py b/src/textual/reactive.py index c2863deab..38d631a65 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -242,9 +242,18 @@ class Reactive(Generic[ReactiveType]): 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: @@ -333,11 +342,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((obj, callback)) if init: current_value = getattr(obj, attribute_name, None) Reactive._check_watchers(obj, attribute_name, current_value)