Skip to content

Commit 1613d7c

Browse files
authored
Merge pull request #4615 from Textualize/fix-notify-error
Fix notifications crash
2 parents 7925273 + d45d19b commit 1613d7c

File tree

12 files changed

+163
-29
lines changed

12 files changed

+163
-29
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ 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+
## [0.65.2] - 2023-06-06
9+
10+
### Fixed
11+
12+
- Fixed issue with notifications and screen switches https://github.com/Textualize/textual/pull/4615
13+
14+
### Added
15+
16+
- Added textual.rlock.RLock https://github.com/Textualize/textual/pull/4615
17+
818
## [0.65.1] - 2024-06-05
919

1020
### Fixed
@@ -2069,6 +2079,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
20692079
- New handler system for messages that doesn't require inheritance
20702080
- Improved traceback handling
20712081

2082+
[0.65.2]: https://github.com/Textualize/textual/compare/v0.65.1...v0.65.2
20722083
[0.65.1]: https://github.com/Textualize/textual/compare/v0.65.0...v0.65.1
20732084
[0.65.0]: https://github.com/Textualize/textual/compare/v0.64.0...v0.65.0
20742085
[0.64.0]: https://github.com/Textualize/textual/compare/v0.63.6...v0.64.0

examples/dictionary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def on_input_changed(self, message: Input.Changed) -> None:
3333
self.lookup_word(message.value)
3434
else:
3535
# Clear the results
36-
self.query_one("#results", Markdown).update("")
36+
await self.query_one("#results", Markdown).update("")
3737

3838
@work(exclusive=True)
3939
async def lookup_word(self, word: str) -> None:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "textual"
3-
version = "0.65.1"
3+
version = "0.65.2"
44
homepage = "https://github.com/Textualize/textual"
55
repository = "https://github.com/Textualize/textual"
66
documentation = "https://textual.textualize.io/"

src/textual/_callback.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def _invoke(callback: Callable, *params: object) -> Any:
4848
return result
4949

5050

51-
async def invoke(callback: Callable[[], Any], *params: object) -> Any:
51+
async def invoke(callback: Callable[..., Any], *params: object) -> Any:
5252
"""Invoke a callback with an arbitrary number of parameters.
5353
5454
Args:

src/textual/app.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
from .notifications import Notification, Notifications, Notify, SeverityLevel
104104
from .reactive import Reactive
105105
from .renderables.blank import Blank
106+
from .rlock import RLock
106107
from .screen import (
107108
ActiveBinding,
108109
Screen,
@@ -579,7 +580,7 @@ def __init__(
579580
else None
580581
)
581582
self._screenshot: str | None = None
582-
self._dom_lock = asyncio.Lock()
583+
self._dom_lock = RLock()
583584
self._dom_ready = False
584585
self._batch_count = 0
585586
self._notifications = Notifications()
@@ -2751,23 +2752,24 @@ def is_mounted(self, widget: Widget) -> bool:
27512752
async def _close_all(self) -> None:
27522753
"""Close all message pumps."""
27532754

2754-
# Close all screens on all stacks:
2755-
for stack in self._screen_stacks.values():
2756-
for stack_screen in reversed(stack):
2757-
if stack_screen._running:
2758-
await self._prune_node(stack_screen)
2759-
stack.clear()
2760-
2761-
# Close pre-defined screens.
2762-
for screen in self.SCREENS.values():
2763-
if isinstance(screen, Screen) and screen._running:
2764-
await self._prune_node(screen)
2765-
2766-
# Close any remaining nodes
2767-
# Should be empty by now
2768-
remaining_nodes = list(self._registry)
2769-
for child in remaining_nodes:
2770-
await child._close_messages()
2755+
async with self._dom_lock:
2756+
# Close all screens on all stacks:
2757+
for stack in self._screen_stacks.values():
2758+
for stack_screen in reversed(stack):
2759+
if stack_screen._running:
2760+
await self._prune_node(stack_screen)
2761+
stack.clear()
2762+
2763+
# Close pre-defined screens.
2764+
for screen in self.SCREENS.values():
2765+
if isinstance(screen, Screen) and screen._running:
2766+
await self._prune_node(screen)
2767+
2768+
# Close any remaining nodes
2769+
# Should be empty by now
2770+
remaining_nodes = list(self._registry)
2771+
for child in remaining_nodes:
2772+
await child._close_messages()
27712773

27722774
async def _shutdown(self) -> None:
27732775
self._begin_batch() # Prevents any layout / repaint while shutting down
@@ -3341,7 +3343,10 @@ async def prune_widgets_task(
33413343
await self._prune_nodes(widgets)
33423344
finally:
33433345
finished_event.set()
3344-
self._update_mouse_over(self.screen)
3346+
try:
3347+
self._update_mouse_over(self.screen)
3348+
except ScreenStackError:
3349+
pass
33453350
if parent is not None:
33463351
parent.refresh(layout=True)
33473352

@@ -3555,7 +3560,7 @@ def _refresh_notifications(self) -> None:
35553560
# or one will turn up. Things will work out later.
35563561
return
35573562
# Update the toast rack.
3558-
toast_rack.show(self._notifications)
3563+
self.call_later(toast_rack.show, self._notifications)
35593564

35603565
def notify(
35613566
self,

src/textual/message_pump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ async def _on_message(self, message: Message) -> None:
746746
if message._sender is not None and message._sender == self._parent:
747747
# parent is sender, so we stop propagation after parent
748748
message.stop()
749-
if self.is_parent_active and not self._parent._closing:
749+
if self.is_parent_active and self.is_attached:
750750
message._bubble_to(self._parent)
751751

752752
def check_idle(self) -> None:

src/textual/rlock.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
from asyncio import Lock, Task, current_task
4+
5+
6+
class RLock:
7+
"""A re-entrant asyncio lock."""
8+
9+
def __init__(self) -> None:
10+
self._owner: Task | None = None
11+
self._count = 0
12+
self._lock = Lock()
13+
14+
async def acquire(self) -> None:
15+
"""Wait until the lock can be acquired."""
16+
task = current_task()
17+
assert task is not None
18+
if self._owner is None or self._owner is not task:
19+
await self._lock.acquire()
20+
self._owner = task
21+
self._count += 1
22+
23+
def release(self) -> None:
24+
"""Release a previously acquired lock."""
25+
task = current_task()
26+
assert task is not None
27+
self._count -= 1
28+
if self._count < 0:
29+
# Should not occur if every acquire as a release
30+
raise RuntimeError("RLock.release called too many times")
31+
if self._owner is task:
32+
if not self._count:
33+
self._owner = None
34+
self._lock.release()
35+
36+
@property
37+
def is_locked(self):
38+
"""Return True if lock is acquired."""
39+
return self._lock.locked()
40+
41+
async def __aenter__(self) -> None:
42+
"""Asynchronous context manager to acquire and release lock."""
43+
await self.acquire()
44+
45+
async def __aexit__(self, _type, _value, _traceback) -> None:
46+
"""Exit the context manager."""
47+
self.release()
48+
49+
50+
if __name__ == "__main__":
51+
from asyncio import Lock
52+
53+
async def locks():
54+
lock = RLock()
55+
async with lock:
56+
async with lock:
57+
print("Hello")
58+
59+
import asyncio
60+
61+
asyncio.run(locks())

src/textual/widget.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import annotations
66

7-
from asyncio import Lock, create_task, wait
7+
from asyncio import create_task, wait
88
from collections import Counter
99
from contextlib import asynccontextmanager
1010
from fractions import Fraction
@@ -81,6 +81,7 @@
8181
from .reactive import Reactive
8282
from .render import measure
8383
from .renderables.blank import Blank
84+
from .rlock import RLock
8485
from .strip import Strip
8586
from .walk import walk_depth_first
8687

@@ -396,7 +397,7 @@ def __init__(
396397
if self.BORDER_SUBTITLE:
397398
self.border_subtitle = self.BORDER_SUBTITLE
398399

399-
self.lock = Lock()
400+
self.lock = RLock()
400401
"""`asyncio` lock to be used to synchronize the state of the widget.
401402
402403
Two different tasks might call methods on a widget at the same time, which
@@ -3550,7 +3551,6 @@ def post_message(self, message: Message) -> bool:
35503551
self.log.warning(self, f"IS NOT RUNNING, {message!r} not sent")
35513552
except NoActiveAppError:
35523553
pass
3553-
35543554
return super().post_message(message)
35553555

35563556
async def _on_idle(self, event: events.Idle) -> None:

src/textual/widgets/_markdown.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,8 @@ async def mount_batch(batch: list[MarkdownBlock]) -> None:
10121012
batch.clear()
10131013
if batch:
10141014
await mount_batch(batch)
1015+
if not removed:
1016+
await markdown_block.remove()
10151017

10161018
self._table_of_contents = table_of_contents
10171019

src/textual/widgets/_toast.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ def show(self, notifications: Notifications) -> None:
183183
Args:
184184
notifications: The notifications to show.
185185
"""
186-
187186
# Look for any stale toasts and remove them.
188187
for toast in self.query(Toast):
189188
if toast._notification not in notifications:

0 commit comments

Comments
 (0)