Skip to content

Commit 11266cb

Browse files
authored
Merge pull request #5848 from Textualize/thread-init
sync primitives
2 parents 7e7ea0c + c6c8b8e commit 11266cb

File tree

4 files changed

+49
-4
lines changed

4 files changed

+49
-4
lines changed

src/textual/app.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,17 @@ def __init_subclass__(cls, *args, **kwargs) -> None:
865865

866866
return super().__init_subclass__(*args, **kwargs)
867867

868+
def _thread_init(self):
869+
"""Initialize threading primitives for the current thread.
870+
871+
https://github.com/Textualize/textual/issues/5845
872+
873+
"""
874+
self._message_queue
875+
self._mounted_event
876+
self._exception_event
877+
self._thread_id = threading.get_ident()
878+
868879
def _get_dom_base(self) -> DOMNode:
869880
"""When querying from the app, we want to query the default screen."""
870881
return self.default_screen
@@ -2094,8 +2105,9 @@ async def run_auto_pilot(
20942105
run_auto_pilot(auto_pilot, pilot), name=repr(pilot)
20952106
)
20962107

2108+
self._thread_init()
2109+
20972110
app._loop = asyncio.get_running_loop()
2098-
app._thread_id = threading.get_ident()
20992111
with app._context():
21002112
try:
21012113
await app._process_messages(
@@ -3120,6 +3132,9 @@ async def _process_messages(
31203132
terminal_size: tuple[int, int] | None = None,
31213133
message_hook: Callable[[Message], None] | None = None,
31223134
) -> None:
3135+
3136+
self._thread_init()
3137+
31233138
async def app_prelude() -> bool:
31243139
"""Work required before running the app.
31253140

src/textual/message_pump.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from __future__ import annotations
1212

1313
import asyncio
14-
import sys
1514
import threading
1615
from asyncio import CancelledError, QueueEmpty, Task, create_task
1716
from contextlib import contextmanager
@@ -23,12 +22,10 @@
2322
Awaitable,
2423
Callable,
2524
Generator,
26-
Generic,
2725
Iterable,
2826
Type,
2927
TypeVar,
3028
cast,
31-
overload,
3229
)
3330
from weakref import WeakSet
3431

@@ -163,6 +160,15 @@ def _prevent_message_types_stack(self) -> list[set[type[Message]]]:
163160
prevent_message_types_stack.set(stack)
164161
return stack
165162

163+
def _thread_init(self):
164+
"""Initialize threading primitives for the current thread.
165+
166+
Require for Python3.8 https://github.com/Textualize/textual/issues/5845
167+
168+
"""
169+
self._message_queue
170+
self._mounted_event
171+
166172
def _get_prevented_messages(self) -> set[type[Message]]:
167173
"""A set of all the prevented message types."""
168174
return self._prevent_message_types_stack[-1]
@@ -506,6 +512,8 @@ async def _close_messages(self, wait: bool = True) -> None:
506512

507513
def _start_messages(self) -> None:
508514
"""Start messages task."""
515+
self._thread_init()
516+
509517
if self.app._running:
510518
self._task = create_task(
511519
self._process_messages(), name=f"message pump {self}"

src/textual/timer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async def _run(self) -> None:
152152
count = 0
153153
_repeat = self._repeat
154154
_interval = self._interval
155+
self._active # Force instantiation in same thread
155156
await self._active.wait()
156157
start = _time.get_time()
157158

tests/test_message_pump.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import threading
2+
13
import pytest
24

35
from textual._dispatch_key import dispatch_key
@@ -169,3 +171,22 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
169171
async with app.run_test() as pilot:
170172
await pilot.click(MyButton)
171173
assert app_button_pressed
174+
175+
176+
async def test_thread_safe_post_message():
177+
class TextMessage(Message):
178+
pass
179+
180+
class TestApp(App):
181+
182+
def on_mount(self) -> None:
183+
msg = TextMessage()
184+
threading.Thread(target=self.post_message, args=(msg,)).start()
185+
186+
def on_text_message(self, message):
187+
self.exit()
188+
189+
app = TestApp()
190+
191+
async with app.run_test() as pilot:
192+
await pilot.pause()

0 commit comments

Comments
 (0)