Giter Club home page Giter Club logo

wxasync's People

Contributors

brendansimon avatar dobatymo avatar enkore avatar joshms123 avatar sirk390 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wxasync's Issues

async def MainLoop

at least on a Windows, the app receives events (Idle and UpdateUI) all the time and it behaves more like the original app by changing the last line to

if IS_MAC or evtloop.Pending(): evtloop.ProcessIdle()

WxAsyncApp must be created before use of AsyncBind is allowed

My app is built as per the wxPython advice

Normally you would derive from this class and implement an OnInit method that creates a frame and then calls self.SetTopWindow(frame), however wx.App is also usable on it’s own without derivation.

I'm trying to convert my application to use wxasync - its a very exciting prospect to potentially be able to introduce concurrency into my wxPython app. However wxasync seems to insist that the WxAsyncApp is created first, and any AsyncBind calls must happen later - say, during the frame creation - as per the main wxasync example.

As mentioned, my app simply isn't structured that way. My app creates a frame within the app's OnInit() and my menu binding (where I want to use AsyncBind) is also done in the app's OnInit(). Unfortunately my attempts to use AsyncBind inside OnInit() result in Exception: Create a 'WxAsyncApp' first.

Do I need to refactor my app and frame into separate objects then wire them together later, in order to successfully use wxasync? Or is there a way I can stay with my existing architecture?

Under Linux custom events don't get processed

Under Linux, custom events don't seem to get processed, whereas they do under Mac.

Ubuntu 18.04, Python 3.7.1, wxPython installed with pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython

To repro: Use my example app from #3 and emit a custom event by pressing either the sync or async button.
custom_event_bug

Under Linux, nothing happens as a result of the wx.PostEvent(self, evt) - though the print statement indicates that the button handler was triggered. Under Mac, the event triggers messages/behaviour OK.

Python 3.8 CancelledError

Python 3.8 included a change to the base class for CancelledError.
See: https://bugs.python.org/issue32528

Just removing '.futures' from line 5 avoids an error in 3.8
from asyncio.futures import CancelledError

Here's the error:
ImportError: cannot import name 'CancelledError' from 'asyncio.futures'

AsyncBind cant differentiate source with wx.Menu and wx.MenuItem

Recently I have come across an issue when I am trying to bind coroutines to choices in a wx.Menu.

I create 2 menu items

ID_CONNECT = wx.NewId()
ID_IDENTIFY = wx.NewId()

        ...

        # Connect Button        
        self.ConnectButton = self.FileMenu.Append(id=ID_CONNECT, item="Connect", helpString="Connect")  # type: wx.MenuItem
        AsyncBind(wx.EVT_MENU, self.ConnectButtonCB, self, id=ID_CONNECT)
        
        # Identify Button        
        self.IdentifyButton= self.FileMenu.Append(id=ID_IDENTIFY, item="Identify", helpString="Identify")  # type: wx.MenuItem
        AsyncBind(wx.EVT_MENU, self.IdentifyButtonCB, self, id=ID_IDENTIFY)

Then I have 2 coroutines:

    async def IdentifyButtonCB(self, event):
        print("Coro IdentifyButtonCB was fired")

    async def ConnectButtonCB(self, event):
        print("Coro ConnectButtonCB was fired")

However when I select either option in the menu, both coroutines are executed. I tried to go about it a few ways using the button as source instead of ID, using ID_Any, and some other things, but no matter what both coroutines get executed.

While testing random things to see if I could get anywhere I did find that my custom ID is getting passed to the coroutines through the event, just for some reason AsyncBind executes everything instead of just the event with matching ID.

I came up with 2 different workarounds because I cannot figure out why they are both getting executed.

  1. Check the event ID in each coro, and only continue if it matches. (Not ideal)
    async def ConnectButtonCB_(self, event: wx.Event):
        if event.GetId() == ID_CONNECT:
            print("Coro ConnectButtonCB was fired")
  1. Use regular Bind, and use a lambda that calls StartCoroutine. (Seems alright)
self.Bind(wx.EVT_MENU, lambda x: StartCoroutine(self.IdentifyButtonCB, self), id=ID_IDENTIFY)

I am not sure if or what I am doing wrong. Thanks for any help!

Add AsyncShowDialogWindowModal

On macOS, the behavior of .ShowWindowModal() is quite useful as it allows dialogs to be shown within their parent window rather than as a separate window
Screen Shot 2022-04-13 at 10 39 16
I am requesting a new function, AsyncShowDialogWindowModal, which behaves just like ShowWindowModal but yields instead of using another thread. The behavior of ShowWindowModal is quite silly - on Mac it doesn't yield and instead uses another thread(?) and then fires an event when it is closed, but on Windows and GTK it does yield and has the same behavior as ShowModal.

This is how I implement it in my code:

import platform
system = platform.system()

# ...

async def show_window_modal(dialog: wx.Dialog):
    if system == "Darwin":
        event = asyncio.Event()
        return_code = None

        def handle_event(dialog_event: wx.WindowModalDialogEvent):
            nonlocal return_code
            return_code = dialog_event.GetReturnCode()
            event.set()

        dialog.Bind(
            event=wx.EVT_WINDOW_MODAL_DIALOG_CLOSED,
            handler=handle_event
        )
        dialog.ShowWindowModal()

        await event.wait()
        return return_code
    else:
       return await wxasync.AsyncShowDialogModal(dialog)

Long event dispatching time leads to high async latency

Detached from #9 (comment)

while Pending():
    Dispatch() / DispatchTimeout(1)

can take a long time for various reasons. A particularly noticeable offender is wxListCtrl in LC_VIRTUAL mode, which in certain X11 configurations (I'm not really sure what causes it, but I'm ready to blame VNC anyway) can cause this loop to take up to ~1.5 seconds(!). Dispatch() and DispatchTimeout() themselves don't usually process a single event. They generally process many events, which in practice may take 0.1-0.2 s in some cases. I've observed this loop processing several thousand events in around a hundred iterations, taking several seconds to complete.

Modal event loops (including context/popup menus) block the main event loop as well.

These "hiccups" don't block the app itself - the event loop is running after all - but coroutines and callbacks are only run when the "MainLoop" coroutine yields, which only happens between these loops.

For gathering data my main loop has now grown into this... uh... neat abomination:

    async def MainLoop(self):
        # inspired by https://github.com/wxWidgets/Phoenix/blob/master/samples/mainloop/mainloop.py
        evtloop = wx.GUIEventLoop()
        with wx.EventLoopActivator(evtloop):
            while not self.exiting:
                if IS_MAC:
                    # evtloop.Pending() just returns True on MacOs
                    evtloop.DispatchTimeout(0)
                else:

                    def e2t(e):
                        s = f'{e.__class__.__name__} ({e.GetId()})'
                        #if e.__class__.__name__ in ('ListEvent',):  # Segfaults :)
                        #    s += f' ({e.GetEventObject().__class__.__name__})'
                        return s
                    st = time.perf_counter()
                    self._events.clear()
                    n = 0
                    while True:
                        s0 = time.perf_counter()
                        res = evtloop.Pending()
                        e0 = time.perf_counter() - s0
                        if e0 > 0.01:
                            logging.error('Pending() took %.3f s', e0)
                        if not res:
                            break

                        n += 1

                        l1 = len(self._events)
                        s1 = time.perf_counter()
                        evtloop.DispatchTimeout(1)
                        e1 = time.perf_counter() - s1
                        if e1 > 0.1:
                            logging.error('Event processing took %.3f s: %s', e1, [e2t(e) for e in self._events[l1:]])
                    el = time.perf_counter() - st
                    from collections import Counter

                    if el > 0.1:
                        c = Counter([e.__class__.__name__ for e in self._events])
                        evts = [f'{k} (x{v})' for k, v in sorted(c.items(), key=lambda s: s[1], reverse=True)]
                        logging.error('UI loop stalled for %.3f s (%d iterations, %d events): %s', el, n, len(self._events), evts) #, [e.__class__.__name__ + f' ({e.GetId()})' for e in self._events])

                await asyncio.sleep(0.005)
                self.ProcessPendingEvents()
                evtloop.ProcessIdle()

This makes it really easy to figure out what the issue is:

[17:52:32,289 ERROR] wxasync: UI loop stalled for 0.272 s (6 iterations, 75 events): ['EraseEvent (x48)', 'Event (x24)', 'ListEvent (x3)']
[17:52:42,756 ERROR] wxasync: UI loop stalled for 1.035 s (65 iterations, 605 events): ['EraseEvent (x360)', 'Event (x180)', 'ListEvent (x45)', 'SetCursorEvent (x10)', 'MouseEvent (x10)']
[17:52:44,772 ERROR] wxasync: UI loop stalled for 1.015 s (48 iterations, 598 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)']
[17:52:59,931 ERROR] wxasync: UI loop stalled for 1.039 s (63 iterations, 615 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)', 'MouseEvent (x9)', 'SetCursorEvent (x8)']
[17:53:01,941 ERROR] wxasync: UI loop stalled for 1.009 s (48 iterations, 585 events): ['EraseEvent (x360)', 'Event (x180)', 'ListEvent (x45)']
[17:53:19,728 ERROR] wxasync: UI loop stalled for 1.036 s (101 iterations, 662 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)', 'SetCursorEvent (x36)', 'MouseEvent (x28)']
[17:53:21,051 ERROR] wxasync: UI loop stalled for 0.315 s (19 iterations, 174 events): ['EraseEvent (x104)', 'Event (x52)', 'ListEvent (x13)', 'SetCursorEvent (x3)', 'MouseEvent (x2)']
[17:53:22,074 ERROR] wxasync: UI loop stalled for 1.018 s (145 iterations, 705 events): ['EraseEvent (x360)', 'Event (x180)', 'SetCursorEvent (x65)', 'MouseEvent (x55)', 'ListEvent (x45)']
[17:53:24,289 ERROR] wxasync: UI loop stalled for 1.012 s (48 iterations, 585 events): ['EraseEvent (x360)', 'Event (x180)', 'ListEvent (x45)']
[17:53:54,083 ERROR] wxasync: UI loop stalled for 1.055 s (70 iterations, 623 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)', 'SetCursorEvent (x15)', 'MouseEvent (x10)']
[17:53:55,316 ERROR] wxasync: UI loop stalled for 0.247 s (14 iterations, 122 events): ['EraseEvent (x72)', 'Event (x36)', 'ListEvent (x9)', 'SetCursorEvent (x3)', 'MouseEvent (x2)']
[17:53:56,372 ERROR] wxasync: UI loop stalled for 1.035 s (53 iterations, 603 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)', 'SetCursorEvent (x3)', 'MouseEvent (x2)']
[17:53:58,595 ERROR] wxasync: UI loop stalled for 1.010 s (48 iterations, 585 events): ['EraseEvent (x360)', 'Event (x180)', 'ListEvent (x45)']
[17:54:19,547 ERROR] wxasync: UI loop stalled for 1.041 s (61 iterations, 612 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)', 'MouseEvent (x8)', 'SetCursorEvent (x6)']
[17:54:21,564 ERROR] wxasync: UI loop stalled for 1.016 s (49 iterations, 598 events): ['EraseEvent (x368)', 'Event (x184)', 'ListEvent (x46)']

Sure enough, the window has a wxListCtrl in LC_VIRTUAL mode with eight columns and 45 or so visible rows. The ListEvent events are EVT_LIST_CACHE_HINT events (ignored in my case). Adding a wxListCtrl to the test case presented in #9 shows the same problem:

import wx
import time

from wxasync import AsyncBind, WxAsyncApp, StartCoroutine
import asyncio
from asyncio.events import get_event_loop


async def foo():
    while True:
        st = time.perf_counter()
        await asyncio.sleep(0.05)
        el = time.perf_counter() - st
        if el > 0.06:
            print(f'await sleep(0.05) took {el:0.3f} s!')


class List(wx.ListCtrl):
    def __init__(self, parent):
        super().__init__(parent, style=wx.LC_REPORT | wx.LC_VIRTUAL)
        for n in range(10):
            self.AppendColumn(f'Column {n+1}')
        self.SetItemCount(25000)

    def OnGetItemText(self, item, column):
        return f'{item} @ {column}'


class BasicFrame(wx.Frame):
    def __init__(self, parent=None, id=-1, title=""):
        wx.Frame.__init__(self, parent, id, title, size=(800,800))
        self.wx_panel = wx.Panel(self)
        self.wx_sizer = wx.BoxSizer(wx.VERTICAL)
        self.left = wx.ListBox(self.wx_panel, name="list1")
        self.wx_sizer.Add(self.left, flag=wx.EXPAND)
        self.right = wx.ListBox(self.wx_panel, name="list2")
        self.wx_sizer.Add(self.right, flag=wx.EXPAND)
        self.text = wx.TextCtrl(self.wx_panel, name="Username")
        self.wx_sizer.Add(self.text, flag=wx.EXPAND)
        self.list = List(self.wx_panel)
        self.wx_sizer.Add(self.list, 1, flag=wx.EXPAND)
        self.wx_panel.SetSizerAndFit(self.wx_sizer)



        # Fills the lists
        self.left.Append("One")
        self.left.Append("Two")
        self.left.Append("Three")
        self.right.Append("First")
        self.right.Append("Second")
        StartCoroutine(foo(), self)

def main():
    class MyApp(wx.App):

        def OnInit(self):
            frame = BasicFrame(title="test")
            frame.Show(True)

            import wx.lib.inspection
            wx.UpdateUIEvent.SetMode(wx.UPDATE_UI_PROCESS_SPECIFIED)
            self.SetTopWindow(frame)

            return True

    app = MyApp(0)
    app.MainLoop()
    # del app

def main_async():
    # see https://github.com/sirk390/wxasync
    app = WxAsyncApp()
    frame = BasicFrame(title="test")
    frame.Show(True)
    app.SetTopWindow(frame)
    loop = get_event_loop()
    loop.run_until_complete(app.MainLoop())

if __name__ == "__main__":
    main_async()

Adding a log call to the FilterEvent method of WxAsyncApp can directly show how slowly these are emitted:

[18:14:49,996 DEBUG] wxasync: ListEvent
[18:14:50,050 DEBUG] wxasync: ListEvent
[18:14:50,079 DEBUG] wxasync: ListEvent
[18:14:50,101 DEBUG] wxasync: ListEvent
[18:14:50,118 DEBUG] wxasync: ListEvent
[18:14:50,147 DEBUG] wxasync: ListEvent
[18:14:50,167 DEBUG] wxasync: ListEvent
[18:14:50,184 DEBUG] wxasync: ListEvent
[18:14:50,218 DEBUG] wxasync: ListEvent
[18:14:50,237 DEBUG] wxasync: ListEvent
[18:14:50,268 DEBUG] wxasync: ListEvent
[18:14:50,287 DEBUG] wxasync: ListEvent
[18:14:50,319 DEBUG] wxasync: ListEvent
[18:14:50,339 DEBUG] wxasync: ListEvent
[18:14:50,369 DEBUG] wxasync: ListEvent
[18:14:50,390 DEBUG] wxasync: ListEvent
[18:14:50,408 DEBUG] wxasync: ListEvent
[18:14:50,438 DEBUG] wxasync: ListEvent
[18:14:50,462 DEBUG] wxasync: ListEvent
[18:14:50,498 DEBUG] wxasync: ListEvent
[18:14:50,532 DEBUG] wxasync: ListEvent
[18:14:50,554 DEBUG] wxasync: ListEvent
[18:14:50,585 DEBUG] wxasync: ListEvent
[18:14:50,605 DEBUG] wxasync: ListEvent
[18:14:50,635 DEBUG] wxasync: ListEvent
[18:14:50,657 DEBUG] wxasync: ListEvent
[18:14:50,688 DEBUG] wxasync: ListEvent
[18:14:50,709 DEBUG] wxasync: ListEvent
[18:14:50,741 DEBUG] wxasync: ListEvent
[18:14:50,761 DEBUG] wxasync: ListEvent
[18:14:50,793 DEBUG] wxasync: ListEvent
[18:14:50,813 DEBUG] wxasync: ListEvent
[18:14:50,844 DEBUG] wxasync: ListEvent
[18:14:50,866 DEBUG] wxasync: ListEvent
[18:14:50,901 DEBUG] wxasync: ListEvent
[18:14:50,932 DEBUG] wxasync: ListEvent
[18:14:50,960 DEBUG] wxasync: ListEvent
[18:14:50,997 DEBUG] wxasync: ListEvent
[18:14:51,018 ERROR] wxasync: UI loop stalled for 1.024 s (40 iterations, 608 events): ['EraseEvent (x380)', 'Event (x190)', 'ListEvent (x38)']
await sleep(0.05) took 1.069 s!

Like I said above, this doesn't just happen on a normal Linux desktop, but only affects certain configurations. (Due to the somewhat absurd slowness I suspect that in my configuration draw calls might be executing synchronously over the network).

Now the interesting question is of course what can we do about this?

Several things:

  1. Yield inside the loop, to limit the damage being done:

    while Pending():
        Dispatch()
        await asyncio.sleep(0)  # == yield None
    

    This isn't particularly clean or smart, but should reduce the latency enough that it isn't immediately obvious to a human. A one or two second hang between "button click" and "results show up on screen" is very obvious, an extra 100 ms less so.

  2. Somehow yield in FilterEvent (careful: might reduce event throughput due to overhead). I'm pretty sure this would be a somewhat ugly hack with asyncio, since it lacks any kind of run_once / tick notion. Maybe asyncio.run_until_complete(asyncio.sleep(0)) would work. Maybe it doesn't.

  3. Ensure perfect coupling [1]. I don't think this is possible with asyncio and wxEventLoop. It's what I built asynker for a few years ago, but it's somewhat annoying, since asynker is very specifically not asyncio, so there is a lot less functionality and no compatibility with 3rd party libs written for asyncio.

[1] Perfect coupling: Event loop A yields to event loop B as soon as B has runnable tasks and vice versa. Perfect coupling is always the case when A and B are identical (that's how asynker does it; it doesn't have a separate event loop). Perfect coupling is very desirable in my opinion, because achieving it coincidentally also tends to mean that there is no additional delay between events in A causing a coroutine in B to become runnable. It also means the overall mechanism of A and B doesn't have to constantly run like MainLoop in wxasync has to do; it only needs to run when there are events to process.

(eref 42d478fb6976)

issue with AsyncShowDialog and AsyncShowDialogModal on macos (abort crash)

I just checked out the latest master and tried running the examples with latest Python 3.9 and wxPython snapshot on macOS.

I tried with Python 3.10 too.

These might be wxPython snapshot issues and/or macOS specific issues?

(venv-3.9) ➜  examples git:(master) % python dialog.py 
The ReturnCode is 5100 and you entered 'asdfasdfasdf'
objc[60344]: autorelease pool page 0x7fda0f8b5000 corrupted
  magic     0x00000000 0x00000000 0x00000000 0x00000000
  should be 0xa1a1a1a1 0x4f545541 0x454c4552 0x21455341
  pthread   0x10e199600
  should be 0x10e199600

[1]    60344 abort      python dialog.py
(venv-3.9) ➜  examples git:(master) % 
(venv-3.9) ➜  examples git:(master) % python more_dialogs.py
2022-08-03 23:16:45.882 Python[60557:3888182] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007ff8181fe7c3 __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007ff817f5ebc3 objc_exception_throw + 48
	2   CoreFoundation                      0x00007ff818227076 -[NSException raise] + 9
	3   AppKit                              0x00007ff81abe30a4 -[NSWindow(NSWindow_Theme) _postWindowNeedsToResetDragMarginsUnlessPostingDisabled] + 321
	4   AppKit                              0x00007ff81abcf054 -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 1288
	5   AppKit                              0x00007ff81ad77d81 -[NSPanel _initContent:styleMask:backing:defer:contentView:] + 50
	6   AppKit                              0x00007ff81abceb46 -[NSWindow initWithContentRect:styleMask:backing:defer:] + 42
	7   AppKit                              0x00007ff81ad77d3a -[NSPanel initWithContentRect:styleMask:backing:defer:] + 59
	8   AppKit                              0x00007ff81b5c3f16 -[NSSavePanel initWithContentRect:styleMask:backing:defer:] + 91
	9   AppKit                              0x00007ff81b5cdb7b -[NSOpenPanel initWithContentRect:styleMask:backing:defer:] + 150
	10  AppKit                              0x00007ff81aeb4078 -[NSPanel init] + 69
	11  AppKit                              0x00007ff81b5c3e98 -[NSSavePanel init] + 197
	12  AppKit                              0x00007ff81b28b8a2 +[NSSavePanel(Instantiation) _crunchyRawUnbonedPanel] + 58
	13  libwx_osx_cocoau_core-3.2.0.0.0.dyl 0x000000010bfa2637 _ZNK11wxDirDialog14OSXCreatePanelEv + 39
	14  libwx_osx_cocoau_core-3.2.0.0.0.dyl 0x000000010bfa29bd _ZN11wxDirDialog9ShowModalEv + 45
	15  _core.cpython-39-darwin.so          0x000000010cc4d9a8 _ZL26meth_wxDirDialog_ShowModalP7_objectS0_ + 104
	16  Python                              0x000000010ac5f896 cfunction_call + 134
	17  Python                              0x000000010ac1c01d _PyObject_Call + 141
	18  Python                              0x000000010ad06d45 _PyEval_EvalFrameDefault + 29989
	19  Python                              0x000000010ac1c283 function_code_fastcall + 163
	20  Python                              0x000000010ad0956d call_function + 413
	21  Python                              0x000000010ad06831 _PyEval_EvalFrameDefault + 28689
	22  Python                              0x000000010ac1c283 function_code_fastcall + 163
	23  Python                              0x000000010ad06d45 _PyEval_EvalFrameDefault + 29989
	24  Python                              0x000000010ac1c283 function_code_fastcall + 163
	25  Python                              0x000000010ad0956d call_function + 413
	26  Python                              0x000000010ad06831 _PyEval_EvalFrameDefault + 28689
	27  Python                              0x000000010ac1c283 function_code_fastcall + 163
	28  Python                              0x000000010ad0956d call_function + 413
	29  Python                              0x000000010ad06831 _PyEval_EvalFrameDefault + 28689
	30  Python                              0x000000010ac1c283 function_code_fastcall + 163
	31  Python                              0x000000010ac1e663 method_vectorcall + 371
	32  Python                              0x000000010adad227 t_bootstrap + 71
	33  Python                              0x000000010ad5b8f9 pythread_wrapper + 25
	34  libsystem_pthread.dylib             0x00007ff8180bc4e1 _pthread_start + 125
	35  libsystem_pthread.dylib             0x00007ff8180b7f6b thread_start + 15
)
libc++abi: terminating with uncaught exception of type NSException
[1]    60557 abort      python more_dialogs.py
(venv-3.9) ➜  examples git:(master) % 

Event Loop appears to block with mouse click and hold on window title bar

Example log below showing captured windows events. Only block when holding down mouse button on title bar.

0 --> <wx._core.IdleEvent object at 0x000001A525A6E4D0> 10008
-31964 --> <wx._core.MouseCaptureChangedEvent object at 0x000001A525A6E4D0> 10104
-31964 --> <wx._core.MoveEvent object at 0x000001A525A6E4D0> 10090
-31964 --> <wx._core.MoveEvent object at 0x000001A525A6E4D0> 10089
-31964 --> <wx._core.MoveEvent object at 0x000001A525A6E4D0> 10088
-31964 --> <wx._core.MoveEvent object at 0x000001A525A6E4D0> 10089
-31964 --> <wx._core.MouseCaptureChangedEvent object at 0x000001A525A6E4D0> 10104
-31964 --> <wx._core.MoveEvent object at 0x000001A525A6E4D0> 10091
2024-03-15 17:40:14,854 - WARNING - asyncio:1891 - Executing <Task pending name='Task-1' coro=<main() running at main.py:59> cb=[_run_until_complete_cb() at pyenv-win\versions\3.10.6\lib\asyncio\base_events.py:184] created at pyenv-win\versions\3.10.6\lib\asyncio\tasks.py:636> took 6.344 seconds
0 --> <wx._core.IdleEvent object at 0x000001A525A6DA20> 10008
0 --> <wx._core.IdleEvent object at 0x000001A525A6DA20> 10008
0 --> <wx._core.IdleEvent object at 0x000001A525A6DA20> 10008

Use of asyncio Streams combined with wxPython

First of all, I'm not posting here an issue. Would it be possible to add an example of a dialog initiated along with a Stream server? I'm struggling to create a GUI that would be able to communicate via an other process. I think that asyncio Streams would feat nicely with my project but I have no clue how to start a server and make the window listening to external events. I'm posting here, because it sounds strange but resources about wxPython and this commun usage are quite rare.

Document python version requirements

Since the setup files don't have any version requirements, please document that the last version working with python 3.6 is wxasync==0.45, and above versions require python 3.7.

I added the version requirement to the setup.py file #24
It doesn't fix it for previous releases though.

shutdown of wxasync raises exception if ever open a frame

My wxasync based app, (wxPython 4.04, Python 3.7.1) is generating exceptions as I shut down. The error happens only if my app ever opens another frame (like the wxPython PrintFramework window or a Help window). I don't get the error immediately though - only when exiting the main app and shutting down the main app/frame.

If I never open another frame from my main wxPython app, then the shutdown happens cleanly.

The two exceptions seem to be related, in that a bound object cannot be found. I haven't had time to create a simple repro case yet.

Traceback (most recent call last):
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 51, in <lambda>
    object.Bind(event_binder, lambda event: self.OnEvent(event, object, event_binder.typeId), id=id, id2=id2)
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 64, in OnEvent
    for asyncallback in self.BoundObjects[obj][type]:
KeyError: <wx._core.Frame object at 0x7fec2cca5c18>

and

Traceback (most recent call last):
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 49, in <lambda>
    object.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, object))
  File "/home/andy/.pyenv/versions/3.7.1/lib/python3.7/site-packages/wxasync.py", line 85, in OnDestroy
    del self.BoundObjects[obj]

wx._core.wxAssertionError with HtmlHelpDialog

Versions
Python 3.11.4 (tags/v3.11.4:d2340ef, Jun  7 2023, 05:45:37) [MSC v.1934 64 bit (AMD64)] on win32
wxasync 0.49
wxPython 4.2.2a1.dev5600+b67eef25 msw (phoenix) wxWidgets 3.2.2.1

Running src/example/more_dialogs.py, clicking on HtmlHelpDialog and closing the dialog results in an exception:

Exception in callback WxAsyncApp.OnTaskCompleted(<Task finishe...ws messages')>)
handle: <Handle WxAsyncApp.OnTaskCompleted(<Task finishe...ws messages')>)>
Traceback (most recent call last):
  File "C:\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Python311\Lib\site-packages\wxasync.py", line 89, in OnTaskCompleted
    _res = task.result()
           ^^^^^^^^^^^^^
  File "D:\EventGhost-2023\EventGhost-async.py", line 111, in on_HtmlHelpDialog
    await self.ShowDialog(dlg)
  File "D:\EventGhost-2023\EventGhost-async.py", line 80, in ShowDialog
    response = await AsyncShowDialogModal(dlg)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\site-packages\wxasync.py", line 166, in AsyncShowDialogModal
    return await ShowModalInExecutor(dlg)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\site-packages\wxasync.py", line 129, in ShowModalInExecutor
    return await loop.run_in_executor(None, dlg.ShowModal)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\concurrent\futures\thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
wx._core.wxAssertionError: C++ assertion "wxThread::IsMain()" failed at ..\..\src\msw\evtloop.cpp(176) in wxGUIEventLoop::Dispatch(): only the main thread can process Windows messages

article on wxasync - draft - please review

I am writing an article on wxPython and wxasync on www.medium.com and have a draft ready for review.

As a courtesy, I'd like to give the authors of wxasync an opportunity to review the article before I publish it. Happy to accept corrections and feedback :-)

If you don't have the time to review, then please close this ticket - no problem!
The draft link is https://medium.com/@abulka/asynchronous-wxpython-gui-apps-using-async-await-c78c667e0872 - I believe you can add comments on medium.com or reply to me in this ticket, or via my email [email protected]

I think the article will generate quite a bit of interest in wxasync, my last article on medium has had thousands of views, though of course wxPython is a more niche interest, I'm sure.

cheers,
Andy

can't stop a started coroutine?

I can't see anyway to stop a coroutine started via StartCoroutine() ? I've got a long running task, which runs it's own "while True" loop, with a condition check, but sometimes it dies, and I'd like to restart it, and it sould sometimes just be easier to ask it to stop than to try and pass flags in to it? Is this something possible via existing asyncio primitives, and I'm just doing it wrong? or is this something that needs to be added? (potentially via a token returned from StartCoroutine() ?)

App.OnExit() isn't called

Versions
Python 3.11.4 (tags/v3.11.4:d2340ef, Jun  7 2023, 05:45:37) [MSC v.1934 64 bit (AMD64)] on win32
wxasync 0.49
wxPython 4.2.2a1.dev5600+b67eef25 msw (phoenix) wxWidgets 3.2.2.1

As the title says, OnExit() isn't called when exiting the app. Without wxasync, OnExit() is called.

wxasync demo code
import wxasync
import wx
import asyncio


class MyApp(wxasync.WxAsyncApp):

    def OnExit(self):
        print("OnExit called")
        return 0


async def main():
    app = MyApp()
    frame = wx.Frame(None)
    frame.Show()
    await app.MainLoop()


asyncio.run(main())
wxPython demo code
import wx


class MyApp(wx.App):

    def OnExit(self):
        print("OnExit called")
        return 0


app = MyApp()
frame = wx.Frame(None)
frame.Show()
app.MainLoop()

StartCoroutine fails if called from OnInit() in 0.42

Launching this demo repro app using wxasync 0.41 works OK. Under 0.42 it fails with an error coroutine 'MainApp.async_callback' was never awaited

import wx
import time
from wxasync import AsyncBind, WxAsyncApp, StartCoroutine
import asyncio
from asyncio.events import get_event_loop
import wx.lib.newevent

SomeNewEvent, EVT_SOME_NEW_EVENT = wx.lib.newevent.NewEvent()

class MainApp(WxAsyncApp):

    def OnInit(self):
        self.frame = wx.Frame(None, -1, "test",)
        self.frame.CreateStatusBar()
        self.frame.Show(True)
        StartCoroutine(self.async_callback, self)
        return True

    async def async_callback(self):
        self.frame.SetStatusText("Button clicked")
        await asyncio.sleep(1)
        self.frame.SetStatusText("Working")
        await asyncio.sleep(1)
        self.frame.SetStatusText("Completed")

def main_async():
    application = MainApp(0)
    loop = get_event_loop()
    loop.run_until_complete(application.MainLoop())

if __name__ == "__main__":
    main_async()

Under wxasync version 0.42 I get

$ python repro042_bug.py 
Traceback (most recent call last):
  File "/Users/Andy/Devel/pynsource/src/repro042_bug.py", line 25, in OnInit
    StartCoroutine(self.async_callback, self)
  File "/Users/Andy/Devel/pynsource/src/wxasync042.py", line 108, in StartCoroutine
    app.StartCoroutine(coroutine, obj)
  File "/Users/Andy/Devel/pynsource/src/wxasync042.py", line 66, in StartCoroutine
    obj.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, obj), obj) # BUG!!
  File "/Users/andy/.pyenv/versions/3.9.5/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/wx/core.py", line 1456, in _EvtHandler_Bind
    assert source is None or hasattr(source, 'GetId')
AssertionError
OnInit returned false, exiting...
sys:1: RuntimeWarning: coroutine 'MainApp.async_callback' was never awaited

caused by the following line of code in wxasync 7c7c5fe

if obj not in self.BoundObjects:
    self.BoundObjects[obj] = defaultdict(list)
    obj.Bind(wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, obj), obj) # <--- cause of the crash

This is causing problems downstream in my app Pynsource.

Sluggishness while using a screenreader (NVDA)

Hi,

I'm grateful for your work. Allowing async/await in wxPython is great.

Unfortunately, while testing, I find a pretty strange sluggishness which I'm unable to explain. Is it because I'm running on Windows? Or because I'm using only the keyboard (as a screen reader users)? Keyboard events would generate and I can understand, for every key I press, four or five events could be sent... actually more than that. Still, that doesn't sound like a lot of event. Yet, I find a performance of about 200ms each time I press a key, enough to be perceptible. I've simplified your example to a very simple window with only three widgets (two list boxes and one text entry). You can see the problem when typing quickly in the text area, or when changing from widget to widget using the tab key (not the mouse).

import wx
import time

from wxasync import AsyncBind, WxAsyncApp, StartCoroutine
import asyncio
from asyncio.events import get_event_loop

ASYNC_VERSION = True

class BasicFrame(wx.Frame):
    def __init__(self, parent=None, id=-1, title=""):
        wx.Frame.__init__(self, parent, id, title, size=(800,800))
        self.wx_panel = wx.Panel(self)
        self.wx_sizer = wx.BoxSizer(wx.VERTICAL)
        self.left = wx.ListBox(self.wx_panel, name="list1")
        self.wx_sizer.Add(self.left)
        self.right = wx.ListBox(self.wx_panel, name="list2")
        self.wx_sizer.Add(self.right)
        self.text = wx.TextCtrl(self.wx_panel, name="Username")
        self.wx_sizer.Add(self.text)
        self.wx_panel.SetSizerAndFit(self.wx_sizer)

        # Fills the lists
        self.left.Append("One")
        self.left.Append("Two")
        self.left.Append("Three")
        self.right.Append("First")
        self.right.Append("Second")

def main():
    class MyApp(wx.App):

        def OnInit(self):
            frame = BasicFrame(title="test")
            frame.Show(True)
            self.SetTopWindow(frame)

            return True

    app = MyApp(0)
    app.MainLoop()
    # del app

def main_async():
    # see https://github.com/sirk390/wxasync
    app = WxAsyncApp()
    frame = BasicFrame(title="test")
    frame.Show(True)
    app.SetTopWindow(frame)
    loop = get_event_loop()
    loop.run_until_complete(app.MainLoop())

if __name__ == "__main__":
    if ASYNC_VERSION:
        main_async()
    else:
        main()

What's causing this delay? I'm not sure. Obviously, if decreasing asyncio.sleep in the main loop to something smaller, this lag decreases and slowly (incredibly slowly) disappears if a small value is set. But then the problem of a small value is the CPU goes up and up to compensate. Lots of overhead. So I was puzzling over this problem and I still don't find a solution. Obviously, it would be helpful if the loop could instead send its events to be processed to an asynchronous loop, for instance. The coroutine responsible for reading the loop would just block until an event is available. But we're still dealing with the same problem: wxPython and another event loop don't get along so well, except if you don't mind the CPU overhead.

But from seeing the code I can't decide what's causing the delay in particular. 5ms for a sleep is definitely not huge. Again, a key press (particularly the tab key which encourages to switch between widgets, hence triggering lots of events) might fire tens of events... but that's still not enough, in theory, to be perceptible by the user. Any help on this would be appreciated!

Thanks in advance,

Vincent

`AsyncShowModal` raises wxAssertionError on Windows

Reproduction

Run the example code at /wxasync/master/src/examples/dialog.py on Windows, but replace AsyncShowDialog with AsyncShowModal.

from wx import TextEntryDialog
from wxasync import AsyncShowModal, WxAsyncApp
from asyncio.events import get_event_loop


async def main():
    """ This functions demonstrate the use of 'AsyncShowDialog' to Show a
        any wx.Dialog asynchronously, and wait for the result.
    """
    dlg = TextEntryDialog(None, "Please enter some text:")
    return_code = await AsyncShowModal(dlg)
    print("The ReturnCode is %s and you entered '%s'" % (return_code, dlg.GetValue()))
    app.ExitMainLoop()


if __name__ == '__main__':
    app = WxAsyncApp()
    loop = get_event_loop()
    loop.create_task(main())
    loop.run_until_complete(app.MainLoop())

This can also be reproduced in an application with multiple frames. I have attempted to reproduce this on macOS Monterey with no success.

Expected Behavior

Pressing buttons or closing the dialog should cause the window to close and the return_code should be returned.

Actual Behavior

When pressing buttons or closing the dialog, the following exception is raised:

Task exception was never retrieved
future: <Task finished name='Task-1' coro=<main() done, defined at main.py:6> exception=wxAssertionError('C++ assertion "wxThread::IsMain()" failed at ..\\..\\src\\msw\\evtloop.cpp(176) in wxGUIEventLoop::Dispatch(): only the main thread can process Windows messages')>
Traceback (most recent call last):
  File "main.py", line 11, in main
    return_code = await AsyncShowModal(dlg)
  File "[...]\lib\site-packages\wxasync.py", line 113, in AsyncShowModal
    return await loop.run_in_executor(None, dlg.ShowModal)
  File "[...]\lib\concurrent\futures\thread.py", line 52, in run
    result = self.fn(*self.args, **self.kwargs)
wx._core.wxAssertionError: C++ assertion "wxThread::IsMain()" failed at ..\..\src\msw\evtloop.cpp(176) in wxGUIEventLoop::Dispatch(): only the main thread can process Windows messages

Impact

This issue completely blocks me from using ShowModal in my async application. I must resort to using AsyncShow, which do not block input of other frames.

macOS performance issues

Hi Sirk,

Let me start by saying this is a great package, thank you!

I am running into some performance issues on macOS; even at idle, with no widgets, the app.MainLoop seems to suck around 8-10% of CPU constantly. I noticed that you recently made a commit to the main loop, with the comment implying there is no way to idle the main loop on a Mac; am I correct in thinking this is why it uses a lot of CPU at idle?

I ran the same program with wx's own main loop and was getting around 0.1% of CPU. If you know of any fixes/workarounds, that would be great. Below is an example:

import asyncio
import wx
import wxasync


class Frame(wx.Frame):
    def __init__(self):
        super().__init__(None, title='')
        text = wx.StaticText(self)
        text.SetLabel('the quick brown fox jumps over the lazy dog')


async def async_main():
    app = wxasync.WxAsyncApp()
    frame = Frame()
    frame.Show()
    app.SetTopWindow(frame)
    await app.MainLoop()  # After running async MainLoop, 8-10% of CPU is used.


def sync_main():
    app = wx.App()
    frame = Frame()
    frame.Show()
    app.SetTopWindow(frame)
    app.MainLoop()  # After running sync MainLoop, 0.1-0.2% of CPU is used.


if __name__ == '__main__':
    run_type = 'async'
    asyncio.run(async_main()) if run_type == 'async' else sync_main()

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.