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

signals #4556

Merged
merged 4 commits into from
May 25, 2024
Merged

signals #4556

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
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
Loading