88from inspect import iscoroutinefunction
99from typing import TYPE_CHECKING
1010
11- from ._coreutils import logger , log_exception
11+ from ._coreutils import logger , log_exception , call_later_from_thread
1212from .utils .asyncs import sleep
1313from .utils import asyncadapter
1414
@@ -29,7 +29,7 @@ class BaseLoop:
2929 """The base class for an event-loop object.
3030
3131 Canvas backends can implement their own loop subclass (like qt and wx do), but a
32- canvas backend can also rely on one of muliple loop implementations (like glfw
32+ canvas backend can also rely on one of multiple loop implementations (like glfw
3333 running on asyncio or trio).
3434
3535 The lifecycle states of a loop are:
@@ -46,7 +46,7 @@ class BaseLoop:
4646 * Stopping the loop (via ``.stop()``) closes the canvases, which will then stop the loop.
4747 * From there it can go back to the ready state (which would call ``_rc_init()`` again).
4848 * In backends like Qt, the native loop can be started without us knowing: state "active".
49- * In interactive settings like an IDE that runs an syncio or Qt loop, the
49+ * In interactive settings like an IDE that runs an asyncio or Qt loop, the
5050 loop can become "active" as soon as the first canvas is created.
5151
5252 """
@@ -176,8 +176,11 @@ async def wrapper():
176176 def call_soon (self , callback : CallbackFunction , * args : Any ) -> None :
177177 """Arrange for a callback to be called as soon as possible.
178178
179- The callback will be called in the next iteration of the event-loop,
180- but other pending events/callbacks may be handled first. Returns None.
179+ The callback will be called in the next iteration of the event-loop, but
180+ other pending events/callbacks may be handled first. Returns None.
181+
182+ Not thread-safe; use ``call_soon_threadsafe()`` for scheduling callbacks
183+ from another thread.
181184 """
182185 if not callable (callback ):
183186 raise TypeError ("call_soon() expects a callable." )
@@ -190,6 +193,22 @@ async def wrapper():
190193
191194 self ._rc_add_task (wrapper , "call_soon" )
192195
196+ def call_soon_threadsafe (self , callback : CallbackFunction , * args : Any ) -> None :
197+ """A thread-safe variant of ``call_soon()``."""
198+
199+ if not callable (callback ):
200+ raise TypeError ("call_soon_threadsafe() expects a callable." )
201+ elif iscoroutinefunction (callback ):
202+ raise TypeError (
203+ "call_soon_threadsafe() expects a normal callable, not an async one."
204+ )
205+
206+ def wrapper ():
207+ with log_exception ("Callback error:" ):
208+ callback (* args )
209+
210+ self ._rc_call_soon_threadsafe (wrapper )
211+
193212 def call_later (self , delay : float , callback : CallbackFunction , * args : Any ) -> None :
194213 """Arrange for a callback to be called after the given delay (in seconds)."""
195214 if delay <= 0 :
@@ -214,7 +233,7 @@ def run(self) -> None:
214233 its fine to start the loop in the normal way.
215234
216235 This call usually blocks, but it can also return immediately, e.g. when there are no
217- canvases, or when the loop is already active (e.g. interactve via IDE).
236+ canvases, or when the loop is already active (e.g. interactive via IDE).
218237 """
219238
220239 # Can we enter the loop?
@@ -360,8 +379,13 @@ def _rc_stop(self):
360379 def _rc_add_task (self , async_func , name ):
361380 """Add an async task to the running loop.
362381
363- This method is optional. A subclass must either implement ``_rc_add_task`` or ``_rc_call_later``.
382+ True async loop-backends (like asyncio and trio) should implement this.
383+ When they do, ``_rc_call_later`` is not used.
384+
385+ Other loop-backends can use the default implementation, which uses the
386+ ``asyncadapter`` which runs coroutines using ``_rc_call_later``.
364387
388+ * If you implement this, make ``_rc_call_later()`` raise an exception.
365389 * Schedule running the task defined by the given co-routine function.
366390 * The name is for debugging purposes only.
367391 * The subclass is responsible for cancelling remaining tasks in _rc_stop.
@@ -374,11 +398,23 @@ def _rc_add_task(self, async_func, name):
374398 def _rc_call_later (self , delay , callback ):
375399 """Method to call a callback in delay number of seconds.
376400
377- This method is optional. A subclass must either implement ``_rc_add_task`` or ``_rc_call_later``.
401+ Backends that implement ``_rc_add_task`` should not implement this.
402+ Other backends can use the default implementation, which uses a
403+ scheduler thread and ``_rc_call_soon_threadsafe``. But they can also
404+ implement this using the loop-backend's own mechanics.
378405
379- * If you implememt this, make ``_rc_add_task()`` call ``super()._rc_add_task()``.
406+ * If you implement this, make ``_rc_add_task()`` call ``super()._rc_add_task()``.
407+ * Take into account that on Windows, timers are usually inaccurate.
380408 * If delay is zero, this should behave like "call_soon".
381- * No need to catch errors from the callback; that's dealt with internally.
409+ * No need to catch errors from the callback; that's dealt with
410+ internally.
382411 * Return None.
383412 """
413+ call_later_from_thread (delay , self ._rc_call_soon_threadsafe , callback )
414+
415+ def _rc_call_soon_threadsafe (self , callback ):
416+ """Method to schedule a callback in the loop's thread.
417+
418+ Must be thread-safe; this may be called from a different thread.
419+ """
384420 raise NotImplementedError ()
0 commit comments