Skip to content

Commit

Permalink
Merge pull request #4556 from Textualize/signal_refactor
Browse files Browse the repository at this point in the history
signals
  • Loading branch information
willmcgugan authored May 25, 2024
2 parents 3446f42 + b76a5c7 commit 5195a57
Show file tree
Hide file tree
Showing 8 changed files with 44 additions and 25 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Added `immediate` switch to `Signal.publish`

## [0.63.3] - 2024-05-24

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1505,7 +1505,7 @@ def refresh_bindings(self) -> None:
See [actions](/guide/actions#dynamic-actions) for how to use this method.
"""
self.call_later(self.screen.refresh_bindings)
self.screen.refresh_bindings()

async def action_toggle(self, attribute_name: str) -> None:
"""Toggle an attribute on the node.
Expand Down
15 changes: 1 addition & 14 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def app(self) -> "App[object]":
return node

@property
def _is_linked_to_app(self) -> bool:
def is_attached(self) -> bool:
"""Is this node linked to the app through the DOM?"""
node: MessagePump | None = self

Expand Down Expand Up @@ -275,19 +275,6 @@ def log(self) -> Logger:
"""
return self.app._logger

@property
def is_attached(self) -> bool:
"""Is the node 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
4 changes: 3 additions & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,9 @@ def _extend_compose(self, widgets: list[Widget]) -> None:

def _on_mount(self, event: events.Mount) -> None:
"""Set up the tooltip-clearing signal when we mount."""
self.screen_layout_refresh_signal.subscribe(self, self._maybe_clear_tooltip)
self.screen_layout_refresh_signal.subscribe(
self, self._maybe_clear_tooltip, immediate=True
)

async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
Expand Down
26 changes: 23 additions & 3 deletions src/textual/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,45 @@ def __rich_repr__(self) -> rich.repr.Result:
yield "name", self._name
yield "subscriptions", list(self._subscriptions.keys())

def subscribe(self, node: MessagePump, callback: SignalCallbackType) -> None:
def subscribe(
self,
node: MessagePump,
callback: SignalCallbackType,
immediate: bool = False,
) -> None:
"""Subscribe a node to this signal.
When the signal is published, the callback will be invoked.
Args:
node: Node to subscribe.
callback: A callback function which takes a single argument and returns anything (return type ignored).
immediate: Invoke the callback immediately on publish if `True`, otherwise post it to the DOM node to be
called once existing messages have been processed.
Raises:
SignalError: Raised when subscribing a non-mounted widget.
"""

if not node.is_running:
raise SignalError(
f"Node must be running to subscribe to a signal (has {node} been mounted)?"
)

if immediate:

def signal_callback(data: object):
"""Invoke the callback immediately."""
callback(data)

else:

def signal_callback(data: object):
"""Post the callback to the node, to call at the next opertunity."""
node.call_next(callback, data)

callbacks = self._subscriptions.setdefault(node, [])
if callback not in callbacks:
callbacks.append(callback)
callbacks.append(signal_callback)

def unsubscribe(self, node: MessagePump) -> None:
"""Unsubscribe a node from this signal.
Expand Down
4 changes: 2 additions & 2 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ def mount(
Only one of ``before`` or ``after`` can be provided. If both are
provided a ``MountError`` will be raised.
"""
if not self._is_linked_to_app:
if not self.is_attached:
raise MountError(f"Can't mount widget(s) before {self!r} is mounted")
# Check for duplicate IDs in the incoming widgets
ids_to_mount = [widget.id for widget in widgets if widget.id is not None]
Expand Down Expand Up @@ -1126,7 +1126,7 @@ async def recompose(self) -> None:
if self._parent is not None:
async with self.batch():
await self.query("*").exclude(".-textual-system").remove()
if self._is_linked_to_app:
if self.is_attached:
await self.mount_all(compose(self))

def _post_register(self, app: App) -> None:
Expand Down
8 changes: 6 additions & 2 deletions src/textual/widgets/_footer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from collections import defaultdict
from typing import TYPE_CHECKING

import rich.repr
from rich.text import Text
Expand All @@ -11,6 +12,9 @@
from ..reactive import reactive
from ..widget import Widget

if TYPE_CHECKING:
from ..screen import Screen


@rich.repr.auto
class FooterKey(Widget):
Expand Down Expand Up @@ -157,9 +161,9 @@ def compose(self) -> ComposeResult:
)

def on_mount(self) -> None:
def bindings_changed(screen) -> None:
async def bindings_changed(screen: Screen) -> None:
if screen is self.screen:
self.call_next(self.recompose)
await self.recompose()

self.screen.bindings_updated_signal.subscribe(self, bindings_changed)

Expand Down
4 changes: 2 additions & 2 deletions tests/test_suspend.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def on_resume(self, _) -> None:
calls.add("resume signal")

def on_mount(self) -> None:
self.app_suspend_signal.subscribe(self, self.on_suspend)
self.app_resume_signal.subscribe(self, self.on_resume)
self.app_suspend_signal.subscribe(self, self.on_suspend, immediate=True)
self.app_resume_signal.subscribe(self, self.on_resume, immediate=True)

async with SuspendApp(driver_class=HeadlessSuspendDriver).run_test(
headless=False
Expand Down

0 comments on commit 5195a57

Please sign in to comment.