-
Notifications
You must be signed in to change notification settings - Fork 517
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
Create wx.lib.asyncio to enable coroutine support #1103
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,336 @@ | ||
#---------------------------------------------------------------------- | ||
# Name: wx.lib.asyncio | ||
# Purpose: Coroutine support for event handlers | ||
# | ||
# Author: XU Guang-zhao 徐广钊 | ||
# | ||
# Created: 8-Dec-2018 | ||
# Copyright: (C) 2018 XU Guang-zhao 徐广钊 | ||
# Licence: wxWidgets license | ||
# | ||
# Tags: documented | ||
# | ||
#---------------------------------------------------------------------- | ||
|
||
""" | ||
Add ``wx.BindAsync()`` and other supporting classes to enable coroutine_ | ||
support for wxPython. | ||
|
||
.. _coroutine: https://docs.python.org/3/library/asyncio-task.html#coroutine | ||
|
||
Description | ||
=========== | ||
Before the introduction of coroutines, developers for GUI applications have to ways | ||
to handle time-consuming tasks. One of them is ``wx.Yield()`` which periodically | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's periodic only if you call wx.Yield periodically. You can just remove "periodically" here |
||
yield the control to wxPython and let events to be processed. This is straitforward | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yield --> yields control to wxPython --> control to the event loop |
||
but performance issues are usually encountered, and users may frequently observe lags | ||
or busy cursors. Another one of them is to launch the task in another thread, however | ||
due to the complexity of GUI frameworks, UI elements can only be updated in the main | ||
thread, leading to the wide usage of ``wx.CallAfter()`` and callbacks. As the program | ||
logic increases, developers will eventually realize that they have already felt into | ||
the so-called "Callback Hell", and they will spend most of their time in taking care of | ||
the program states among these callbacks, because the program logic is usually in series | ||
but the program must be implemented in an asynchronous way. Considering such a situation: | ||
When a button is clicked a file will be downloaded, and then a CPU-intensive thread will | ||
be launched to process the data. With bare ``wx.CallAfter()`` there will be at least two | ||
callbacks, and what will happen if we need more, for example retrying the download when | ||
it fails? | ||
|
||
Coroutines are introduced to solve this problem. Time-consuming or IO-bound tasks can be | ||
wrapped into "awaitables" and ``await``-ed in coroutines; a coroutine will be | ||
automatically suspended when it is trapped in an "awaitable", and woke up when the task | ||
finishes. Additionally, as all coroutines (all coroutines launched in the main event | ||
loop, exactly) will be executed in the main thread, there will be no worry about any | ||
confilction caused by concurrency in the program logic. | ||
|
||
Usage | ||
===== | ||
Sample usage:: | ||
#!/usr/bin/env python3 | ||
import asyncio | ||
import threading | ||
import time | ||
|
||
import wx | ||
import wx.lib.asyncio | ||
|
||
def main(): | ||
asyncio.get_event_loop().set_debug(True) | ||
|
||
def _another_loop_thread(): | ||
nonlocal another_loop | ||
another_loop = asyncio.new_event_loop() | ||
asyncio.set_event_loop(another_loop) | ||
another_loop.run_forever() | ||
another_loop.close() | ||
|
||
another_loop = None | ||
another_loop_thread = threading.Thread(target=_another_loop_thread) | ||
another_loop_thread.start() | ||
|
||
def on_close(event): | ||
another_loop.call_soon_threadsafe(another_loop.stop) | ||
frame.Destroy() | ||
|
||
frame = wx.Frame(None, title='Coroutine Integration in wxPython', size=wx.Size(800, 600)) | ||
frame.Bind(wx.EVT_CLOSE, on_close) | ||
frame.CreateStatusBar() | ||
frame.GetStatusBar().StatusText = 'Ready' | ||
counter = 1 | ||
|
||
async def on_click(event): | ||
def log(message: str): | ||
frame.GetStatusBar().StatusText = message | ||
print(message) | ||
|
||
nonlocal counter | ||
count = ' [' + str(counter) + ']' | ||
counter += 1 | ||
log('Starting the event handler' + count) | ||
await asyncio.sleep(1) # Sleep in the current event loop | ||
log('Running in the thread pool' + count) | ||
# time.sleep is used to emulate synchronous time-consuming tasks | ||
await asyncio.get_event_loop().run_in_executor(None, time.sleep, 1) | ||
log('Running in another loop' + count) | ||
# Socket operations are theoretically unsupported in WxEventLoop | ||
# So a default event loop in a separate thread is sometime required | ||
# asyncio.sleep is used to emulate these asynchronous tasks | ||
await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(asyncio.sleep(1), another_loop)) | ||
log('Ready' + count) | ||
|
||
button = wx.Button(frame, label='\n'.join([ | ||
'Click to start the asynchronous event handler', | ||
'The application will remain responsive while the handler is running', | ||
'Try click here multiple times to launch multiple coroutines', | ||
'These coroutines will not conflict each other as they are all in the same thread', | ||
])) | ||
button.BindAsync(wx.EVT_BUTTON, on_click) | ||
|
||
frame.Show() | ||
|
||
asyncio.get_event_loop().run_forever() | ||
another_loop_thread.join() | ||
|
||
|
||
if __name__ == '__main__': | ||
asyncio.set_event_loop_policy(wx.lib.asyncio.WxEventLoopPolicy(app=wx.App)) | ||
main() | ||
|
||
""" | ||
|
||
#!/usr/bin/env python3 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this line. It only has effect as the first line in the file, and probably should not be in library modules anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the shebang should only reference itself on the firstline. Remove this otherwise you could have a possible security division/injection issue. |
||
import asyncio | ||
import concurrent.futures | ||
import threading | ||
import time | ||
from asyncio.events import AbstractEventLoop | ||
from asyncio.futures import Future | ||
from typing import Optional, Callable, Any, Type | ||
|
||
import wx | ||
|
||
|
||
class WxTimerHandle(asyncio.TimerHandle): | ||
__slots__ = 'call_later', | ||
|
||
|
||
class WxEventLoop(asyncio.AbstractEventLoop): | ||
def __init__(self, app: wx.AppConsole): | ||
self._closed = False | ||
self._app = app | ||
self._default_executor = None | ||
self._debug = False | ||
self._exception_handler = None | ||
self._task_factory = None | ||
|
||
def run_forever(self) -> None: | ||
self._app.MainLoop() | ||
|
||
def stop(self) -> None: | ||
self._app.ExitMainLoop() | ||
|
||
def is_running(self) -> bool: | ||
return self._app.GetMainLoop() is not None | ||
|
||
def close(self) -> None: | ||
executor = self._default_executor | ||
if executor is not None: | ||
self._default_executor = None | ||
executor.shutdown(wait=False) | ||
self._closed = True | ||
|
||
def _timer_handle_cancelled(self, handle: WxTimerHandle) -> None: | ||
handle.call_later.Stop() | ||
|
||
def call_soon(self, callback: Callable[..., Any], *args, context=None) -> None: | ||
self.call_soon_threadsafe(callback, *args) | ||
|
||
def call_at(self, when, callback: Callable[..., Any], *args, context=None) -> WxTimerHandle: | ||
return self.call_later(when - self.time(), callback, *args, context) | ||
|
||
def call_later(self, delay: float, callback: Callable[..., Any], *args: Any) -> WxTimerHandle: | ||
handle = WxTimerHandle(delay * 1000 + self.time(), callback, args, self) | ||
handle.call_later = wx.CallLater(int(delay * 1000), callback, *args) | ||
return handle | ||
|
||
def time(self) -> float: | ||
return time.monotonic() | ||
|
||
def create_future(self) -> asyncio.Future: | ||
return asyncio.Future(loop=self) | ||
|
||
def create_task(self, coro) -> asyncio.Task: | ||
if self._task_factory is None: | ||
return asyncio.Task(coro, loop=self) | ||
else: | ||
return self._task_factory(self, coro) | ||
|
||
def call_soon_threadsafe(self, callback: Callable[..., Any], *args, context=None) -> None: | ||
wx.CallAfter(callback, *args) | ||
|
||
def run_in_executor(self, executor: concurrent.futures.ThreadPoolExecutor, func: Callable[..., Any], *args) -> asyncio.Future: | ||
if executor is None: | ||
executor = self._default_executor | ||
if executor is None: | ||
executor = concurrent.futures.ThreadPoolExecutor() | ||
self._default_executor = executor | ||
return asyncio.wrap_future(executor.submit(func, *args), loop=self) | ||
|
||
def set_default_executor(self, executor: concurrent.futures.ThreadPoolExecutor) -> None: | ||
self._default_executor = executor | ||
|
||
def get_exception_handler(self): | ||
return self._exception_handler | ||
|
||
def set_exception_handler(self, handler): | ||
self._exception_handler = handler | ||
|
||
def default_exception_handler(self, context): | ||
print('Got exception: ' + repr(context)) | ||
|
||
def call_exception_handler(self, context): | ||
if self._exception_handler is None: | ||
self.default_exception_handler(context) | ||
else: | ||
self._exception_handler(self, context) | ||
|
||
def get_debug(self) -> bool: | ||
return self._debug | ||
|
||
def set_debug(self, enabled: bool) -> None: | ||
self._debug = enabled | ||
|
||
def run_until_complete(self, future): | ||
raise NotImplementedError | ||
|
||
def is_closed(self) -> bool: | ||
return self._closed | ||
|
||
async def shutdown_asyncgens(self): | ||
raise NotImplementedError | ||
|
||
def set_task_factory(self, factory) -> None: | ||
self._task_factory = factory | ||
|
||
def get_task_factory(self): | ||
return self._task_factory | ||
|
||
|
||
class WxEventLoopPolicy(asyncio.AbstractEventLoopPolicy): | ||
def __init__(self, app: Type[wx.AppConsole], delegate: asyncio.AbstractEventLoopPolicy = asyncio.get_event_loop_policy()): | ||
self._app = app | ||
self._loop = None | ||
self._delegate = delegate | ||
|
||
def get_event_loop(self) -> AbstractEventLoop: | ||
if threading.current_thread() is threading.main_thread(): | ||
if self._loop is None: | ||
self._loop = WxEventLoop(self._app()) | ||
return self._loop | ||
else: | ||
return self._delegate.get_event_loop() | ||
|
||
def set_event_loop(self, loop: AbstractEventLoop) -> None: | ||
self._delegate.set_event_loop(loop) | ||
|
||
def new_event_loop(self) -> AbstractEventLoop: | ||
return self._delegate.new_event_loop() | ||
|
||
def get_child_watcher(self) -> Any: | ||
return self._delegate.get_child_watcher() | ||
|
||
def set_child_watcher(self, watcher: Any) -> None: | ||
self._delegate.set_child_watcher(watcher) | ||
|
||
|
||
def _bind_async(self, event, handler): | ||
def _handler(event): | ||
asyncio.ensure_future(handler(event)) | ||
|
||
self.Bind(event, _handler) | ||
|
||
|
||
wx.EvtHandler.BindAsync = _bind_async | ||
|
||
|
||
def _test(): | ||
asyncio.set_event_loop_policy(WxEventLoopPolicy(app=wx.App)) | ||
asyncio.get_event_loop().set_debug(True) | ||
|
||
def _another_loop_thread(): | ||
nonlocal another_loop | ||
another_loop = asyncio.new_event_loop() | ||
asyncio.set_event_loop(another_loop) | ||
another_loop.run_forever() | ||
another_loop.close() | ||
|
||
another_loop = None | ||
another_loop_thread = threading.Thread(target=_another_loop_thread) | ||
another_loop_thread.start() | ||
|
||
def on_close(event): | ||
another_loop.call_soon_threadsafe(another_loop.stop) | ||
frame.Destroy() | ||
|
||
frame = wx.Frame(None, title='Coroutine Integration in wxPython', size=wx.Size(800, 600)) | ||
frame.Bind(wx.EVT_CLOSE, on_close) | ||
frame.CreateStatusBar() | ||
frame.GetStatusBar().StatusText = 'Ready' | ||
counter = 1 | ||
|
||
async def on_click(event): | ||
def log(message: str): | ||
frame.GetStatusBar().StatusText = message | ||
print(message) | ||
|
||
nonlocal counter | ||
count = ' [' + str(counter) + ']' | ||
counter += 1 | ||
log('Starting the event handler' + count) | ||
await asyncio.sleep(1) # Sleep in the current event loop | ||
log('Running in the thread pool' + count) | ||
# time.sleep is used to emulate synchronous time-consuming tasks | ||
await asyncio.get_event_loop().run_in_executor(None, time.sleep, 1) | ||
log('Running in another loop' + count) | ||
# Socket operations are theoretically unsupported in WxEventLoop | ||
# So a default event loop in a separate thread is sometime required | ||
# asyncio.sleep is used to emulate these asynchronous tasks | ||
await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(asyncio.sleep(1), another_loop)) | ||
log('Ready' + count) | ||
|
||
button = wx.Button(frame, label='\n'.join([ | ||
'Click to start the asynchronous event handler', | ||
'The application will remain responsive while the handler is running', | ||
'Try click here multiple times to launch multiple coroutines', | ||
'These coroutines will not conflict each other as they are all in the same thread', | ||
])) | ||
button.BindAsync(wx.EVT_BUTTON, on_click) | ||
|
||
frame.Show() | ||
|
||
asyncio.get_event_loop().run_forever() | ||
another_loop_thread.join() | ||
|
||
|
||
if __name__ == '__main__': | ||
_test() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to --> two