Skip to content
Merged
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
17 changes: 16 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,17 @@ def __init_subclass__(cls, *args, **kwargs) -> None:

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

def _thread_init(self):
"""Initialize threading primitives for the current thread.

https://github.com/Textualize/textual/issues/5845

"""
self._message_queue
self._mounted_event
self._exception_event
self._thread_id = threading.get_ident()

def _get_dom_base(self) -> DOMNode:
"""When querying from the app, we want to query the default screen."""
return self.default_screen
Expand Down Expand Up @@ -2094,8 +2105,9 @@ async def run_auto_pilot(
run_auto_pilot(auto_pilot, pilot), name=repr(pilot)
)

self._thread_init()

app._loop = asyncio.get_running_loop()
app._thread_id = threading.get_ident()
with app._context():
try:
await app._process_messages(
Expand Down Expand Up @@ -3120,6 +3132,9 @@ async def _process_messages(
terminal_size: tuple[int, int] | None = None,
message_hook: Callable[[Message], None] | None = None,
) -> None:

self._thread_init()

async def app_prelude() -> bool:
"""Work required before running the app.

Expand Down
14 changes: 11 additions & 3 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from __future__ import annotations

import asyncio
import sys
import threading
from asyncio import CancelledError, QueueEmpty, Task, create_task
from contextlib import contextmanager
Expand All @@ -23,12 +22,10 @@
Awaitable,
Callable,
Generator,
Generic,
Iterable,
Type,
TypeVar,
cast,
overload,
)
from weakref import WeakSet

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

def _thread_init(self):
"""Initialize threading primitives for the current thread.

Require for Python3.8 https://github.com/Textualize/textual/issues/5845

"""
self._message_queue
self._mounted_event

def _get_prevented_messages(self) -> set[type[Message]]:
"""A set of all the prevented message types."""
return self._prevent_message_types_stack[-1]
Expand Down Expand Up @@ -506,6 +512,8 @@ async def _close_messages(self, wait: bool = True) -> None:

def _start_messages(self) -> None:
"""Start messages task."""
self._thread_init()

if self.app._running:
self._task = create_task(
self._process_messages(), name=f"message pump {self}"
Expand Down
1 change: 1 addition & 0 deletions src/textual/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ async def _run(self) -> None:
count = 0
_repeat = self._repeat
_interval = self._interval
self._active # Force instantiation in same thread
await self._active.wait()
start = _time.get_time()

Expand Down
21 changes: 21 additions & 0 deletions tests/test_message_pump.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import threading

import pytest

from textual._dispatch_key import dispatch_key
Expand Down Expand Up @@ -169,3 +171,22 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
async with app.run_test() as pilot:
await pilot.click(MyButton)
assert app_button_pressed


async def test_thread_safe_post_message():
class TextMessage(Message):
pass

class TestApp(App):

def on_mount(self) -> None:
msg = TextMessage()
threading.Thread(target=self.post_message, args=(msg,)).start()

def on_text_message(self, message):
self.exit()

app = TestApp()

async with app.run_test() as pilot:
await pilot.pause()
Loading