aiowx 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aiowx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.3
2
+ Name: aiowx
3
+ Version: 0.1.0
4
+ Summary: Async I/O bridge for wxPython — run asyncio coroutines with wx GUI
5
+ Author: Rowell Urbaez Reyes
6
+ Author-email: Rowell Urbaez Reyes <167712855+Row0902@users.noreply.github.com>
7
+ Requires-Dist: wxpython>=4.2.5
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # aiowx
12
+ ## asyncio support for wxPython
13
+
14
+ aiowx is a library for using Python 3 asyncio (`async`/`await`) with wxPython.
15
+ The library polls UI messages every 20ms and runs the asyncio message loop the rest of the time.
16
+ When idle, the CPU usage is 0% on Windows and about 1-2% on macOS.
17
+
18
+ ### Installation
19
+
20
+ ```sh
21
+ pip install aiowx
22
+ ```
23
+
24
+ Install using:
25
+ ```sh
26
+ pip install aiowx
27
+ ```
28
+ ### Usage
29
+ Create a **WxAsyncApp** instead of a **wx.App**
30
+
31
+ ```python
32
+ app = WxAsyncApp()
33
+ ```
34
+
35
+ and use **AsyncBind** to bind an event to a coroutine.
36
+ ```python
37
+ async def async_callback():
38
+ (...your code...)
39
+
40
+ AsyncBind(wx.EVT_BUTTON, async_callback, button1)
41
+ ```
42
+ You can still use wx.Bind together with AsyncBind.
43
+
44
+ If you don't want to wait for an event, you just use **StartCoroutine** and it will be executed immediatly.
45
+ It will return an asyncio.Task in case you need to cancel it.
46
+ ```
47
+ task = StartCoroutine(update_clock_coroutine, frame)
48
+ ```
49
+ If you need to stop it run:
50
+ ```
51
+ task.cancel()
52
+ ```
53
+ Any coroutine started using **AsyncBind** or using **StartCoroutine** is attached to a wx.Window. It is automatically cancelled when the Window is destroyed. This makes it easier to use, as you don't need to take care of cancelling them yourselve.
54
+
55
+ To show a Dialog, use **AsyncShowDialog** or **AsyncShowDialogModal**. This allows
56
+ to use 'await' to wait until the dialog completes. Don't use dlg.ShowModal() directly as it would block the event loop.
57
+
58
+ You start the application using:
59
+ ```python
60
+ await app.MainLoop()
61
+ ```
62
+
63
+ Below is full example with AsyncBind, WxAsyncApp, and StartCoroutine:
64
+
65
+ ```python
66
+ import wx
67
+ from aiowx import AsyncBind, WxAsyncApp, StartCoroutine
68
+ import asyncio
69
+ import time
70
+
71
+
72
+ class TestFrame(wx.Frame):
73
+ def __init__(self, parent=None):
74
+ super(TestFrame, self).__init__(parent)
75
+ vbox = wx.BoxSizer(wx.VERTICAL)
76
+ button1 = wx.Button(self, label="Submit")
77
+ self.edit = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
78
+ self.edit_timer = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
79
+ vbox.Add(button1, 2, wx.EXPAND|wx.ALL)
80
+ vbox.AddStretchSpacer(1)
81
+ vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL)
82
+ vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL)
83
+ self.SetSizer(vbox)
84
+ self.Layout()
85
+ AsyncBind(wx.EVT_BUTTON, self.async_callback, button1)
86
+ StartCoroutine(self.update_clock, self)
87
+
88
+ async def async_callback(self, event):
89
+ self.edit.SetLabel("Button clicked")
90
+ await asyncio.sleep(1)
91
+ self.edit.SetLabel("Working")
92
+ await asyncio.sleep(1)
93
+ self.edit.SetLabel("Completed")
94
+
95
+ async def update_clock(self):
96
+ while True:
97
+ self.edit_timer.SetLabel(time.strftime('%H:%M:%S'))
98
+ await asyncio.sleep(0.5)
99
+
100
+
101
+ async def main():
102
+ app = WxAsyncApp()
103
+ frame = TestFrame()
104
+ frame.Show()
105
+ app.SetTopWindow(frame)
106
+ await app.MainLoop()
107
+
108
+
109
+ asyncio.run(main())
110
+
111
+ ```
112
+
113
+ ## Performance
114
+
115
+ Below is view of the performances (on windows Core I7-7700K 4.2Ghz):
116
+
117
+ | Scenario |Latency | Latency (at max throughput)| Max Throughput(msg/s) |
118
+ | ------------- |--------------|---------------------------------|-------------|
119
+ | asyncio only (for reference) |0ms |17ms |571 325|
120
+ | wx only (for reference) |0ms |19ms |94 591|
121
+ | aiowx (GUI) | 5ms |19ms |52 304|
122
+ | aiowx (GUI+asyncio)| 5ms GUI / 0ms asyncio |24ms GUI / 12ms asyncio |40 302 GUI + 134 000 asyncio|
123
+
124
+
125
+ The performance tests are included in the 'test' directory.
126
+
127
+ ## Repository
128
+
129
+ <https://github.com/Row0902/aiowx>
aiowx-0.1.0/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # aiowx
2
+ ## asyncio support for wxPython
3
+
4
+ aiowx is a library for using Python 3 asyncio (`async`/`await`) with wxPython.
5
+ The library polls UI messages every 20ms and runs the asyncio message loop the rest of the time.
6
+ When idle, the CPU usage is 0% on Windows and about 1-2% on macOS.
7
+
8
+ ### Installation
9
+
10
+ ```sh
11
+ pip install aiowx
12
+ ```
13
+
14
+ Install using:
15
+ ```sh
16
+ pip install aiowx
17
+ ```
18
+ ### Usage
19
+ Create a **WxAsyncApp** instead of a **wx.App**
20
+
21
+ ```python
22
+ app = WxAsyncApp()
23
+ ```
24
+
25
+ and use **AsyncBind** to bind an event to a coroutine.
26
+ ```python
27
+ async def async_callback():
28
+ (...your code...)
29
+
30
+ AsyncBind(wx.EVT_BUTTON, async_callback, button1)
31
+ ```
32
+ You can still use wx.Bind together with AsyncBind.
33
+
34
+ If you don't want to wait for an event, you just use **StartCoroutine** and it will be executed immediatly.
35
+ It will return an asyncio.Task in case you need to cancel it.
36
+ ```
37
+ task = StartCoroutine(update_clock_coroutine, frame)
38
+ ```
39
+ If you need to stop it run:
40
+ ```
41
+ task.cancel()
42
+ ```
43
+ Any coroutine started using **AsyncBind** or using **StartCoroutine** is attached to a wx.Window. It is automatically cancelled when the Window is destroyed. This makes it easier to use, as you don't need to take care of cancelling them yourselve.
44
+
45
+ To show a Dialog, use **AsyncShowDialog** or **AsyncShowDialogModal**. This allows
46
+ to use 'await' to wait until the dialog completes. Don't use dlg.ShowModal() directly as it would block the event loop.
47
+
48
+ You start the application using:
49
+ ```python
50
+ await app.MainLoop()
51
+ ```
52
+
53
+ Below is full example with AsyncBind, WxAsyncApp, and StartCoroutine:
54
+
55
+ ```python
56
+ import wx
57
+ from aiowx import AsyncBind, WxAsyncApp, StartCoroutine
58
+ import asyncio
59
+ import time
60
+
61
+
62
+ class TestFrame(wx.Frame):
63
+ def __init__(self, parent=None):
64
+ super(TestFrame, self).__init__(parent)
65
+ vbox = wx.BoxSizer(wx.VERTICAL)
66
+ button1 = wx.Button(self, label="Submit")
67
+ self.edit = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
68
+ self.edit_timer = wx.StaticText(self, style=wx.ALIGN_CENTRE_HORIZONTAL|wx.ST_NO_AUTORESIZE)
69
+ vbox.Add(button1, 2, wx.EXPAND|wx.ALL)
70
+ vbox.AddStretchSpacer(1)
71
+ vbox.Add(self.edit, 1, wx.EXPAND|wx.ALL)
72
+ vbox.Add(self.edit_timer, 1, wx.EXPAND|wx.ALL)
73
+ self.SetSizer(vbox)
74
+ self.Layout()
75
+ AsyncBind(wx.EVT_BUTTON, self.async_callback, button1)
76
+ StartCoroutine(self.update_clock, self)
77
+
78
+ async def async_callback(self, event):
79
+ self.edit.SetLabel("Button clicked")
80
+ await asyncio.sleep(1)
81
+ self.edit.SetLabel("Working")
82
+ await asyncio.sleep(1)
83
+ self.edit.SetLabel("Completed")
84
+
85
+ async def update_clock(self):
86
+ while True:
87
+ self.edit_timer.SetLabel(time.strftime('%H:%M:%S'))
88
+ await asyncio.sleep(0.5)
89
+
90
+
91
+ async def main():
92
+ app = WxAsyncApp()
93
+ frame = TestFrame()
94
+ frame.Show()
95
+ app.SetTopWindow(frame)
96
+ await app.MainLoop()
97
+
98
+
99
+ asyncio.run(main())
100
+
101
+ ```
102
+
103
+ ## Performance
104
+
105
+ Below is view of the performances (on windows Core I7-7700K 4.2Ghz):
106
+
107
+ | Scenario |Latency | Latency (at max throughput)| Max Throughput(msg/s) |
108
+ | ------------- |--------------|---------------------------------|-------------|
109
+ | asyncio only (for reference) |0ms |17ms |571 325|
110
+ | wx only (for reference) |0ms |19ms |94 591|
111
+ | aiowx (GUI) | 5ms |19ms |52 304|
112
+ | aiowx (GUI+asyncio)| 5ms GUI / 0ms asyncio |24ms GUI / 12ms asyncio |40 302 GUI + 134 000 asyncio|
113
+
114
+
115
+ The performance tests are included in the 'test' directory.
116
+
117
+ ## Repository
118
+
119
+ <https://github.com/Row0902/aiowx>
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "aiowx"
3
+ version = "0.1.0"
4
+ description = "Async I/O bridge for wxPython — run asyncio coroutines with wx GUI"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Rowell Urbaez Reyes", email = "167712855+Row0902@users.noreply.github.com" },
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = ["wxpython>=4.2.5"]
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.11.21,<0.12.0"]
14
+ build-backend = "uv_build"
15
+
16
+ [tool.pytest.ini_options]
17
+ asyncio_mode = "auto"
18
+ testpaths = ["tests"]
19
+
20
+ [tool.coverage.run]
21
+ omit = ["src/examples/*"]
22
+
23
+ [tool.ruff]
24
+ exclude = [
25
+ "test", # legacy perf tests / PoC (archived in Phase 3)
26
+ "src/examples", # example scripts requiring wxPython display
27
+ ]
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=9.1.0",
32
+ "pytest-asyncio>=1.4.0",
33
+ "pytest-cov>=7.1.0",
34
+ "ruff>=0.15.17",
35
+ "ty>=0.0.49",
36
+ ]
@@ -0,0 +1,15 @@
1
+ from aiowx._core import (
2
+ AsyncBind,
3
+ AsyncShowDialog,
4
+ AsyncShowDialogModal,
5
+ StartCoroutine,
6
+ WxAsyncApp,
7
+ )
8
+
9
+ __all__ = [
10
+ "AsyncBind",
11
+ "AsyncShowDialog",
12
+ "AsyncShowDialogModal",
13
+ "StartCoroutine",
14
+ "WxAsyncApp",
15
+ ]
@@ -0,0 +1,326 @@
1
+ """Core module for aiowx — bridges wxPython GUI event loop with asyncio.
2
+
3
+ Provides WxAsyncApp, AsyncBind, StartCoroutine, and async dialog helpers
4
+ to run coroutines alongside wxPython's event-driven main loop.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import inspect
11
+ import platform
12
+ import warnings
13
+ from asyncio import CancelledError
14
+ from collections import defaultdict
15
+ from collections.abc import Callable, Coroutine
16
+ from typing import Any, TypeAlias
17
+
18
+ import wx
19
+ import wx.html
20
+
21
+ IS_MAC: bool = platform.system() == "Darwin"
22
+
23
+ CoroutineFn: TypeAlias = Callable[..., Coroutine[Any, Any, Any]]
24
+
25
+
26
+ class WxAsyncApp(wx.App):
27
+ """Wx.App subclass that runs an async main loop alongside wxPython's event loop.
28
+
29
+ Attributes:
30
+ BoundObjects: Tracks registered event bindings per window for cleanup on destroy.
31
+ RunningTasks: Tracks active asyncio tasks per window for cancellation on destroy.
32
+ exiting: Flag to signal MainLoop to stop.
33
+ ui_idle: Tracks whether idle processing is pending.
34
+ sleep_duration: Sleep interval between wx event processing cycles.
35
+ warn_on_cancel_callback: If True, emit warning when canceling a callback on destroy.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ warn_on_cancel_callback: bool = False,
41
+ sleep_duration: float = 0.02,
42
+ **kwargs: Any,
43
+ ) -> None:
44
+ self.BoundObjects: dict[wx.Window, dict[int, list[Callable[..., Any]]]] = {}
45
+ self.RunningTasks: defaultdict[wx.Window, set[asyncio.Task[Any]]] = defaultdict(
46
+ set
47
+ )
48
+ self.exiting: bool = False
49
+ self.ui_idle: bool = True
50
+ self.sleep_duration: float = sleep_duration
51
+ self.warn_on_cancel_callback: bool = warn_on_cancel_callback
52
+ super().__init__(**kwargs)
53
+ self.SetExitOnFrameDelete(True)
54
+
55
+ async def MainLoop(self) -> None:
56
+ """Run the wxPython event loop interleaved with asyncio.
57
+
58
+ Polls wx.GUIEventLoop for pending events on non-Mac platforms.
59
+ On Mac, uses DispatchTimeout(0) because Pending() always returns True.
60
+ Exits when ExitMainLoop() sets the exiting flag.
61
+ """
62
+ event_loop = wx.GUIEventLoop()
63
+ with wx.EventLoopActivator(event_loop):
64
+ while not self.exiting:
65
+ if IS_MAC:
66
+ event_loop.DispatchTimeout(0)
67
+ self.ui_idle = False
68
+ else:
69
+ while event_loop.Pending():
70
+ event_loop.Dispatch()
71
+ await asyncio.sleep(0)
72
+ self.ui_idle = False
73
+ await asyncio.sleep(self.sleep_duration)
74
+ self.ProcessPendingEvents()
75
+ if not self.ui_idle:
76
+ event_loop.ProcessIdle()
77
+ self.ui_idle = True
78
+ self.exiting = False
79
+ self.OnExit()
80
+
81
+ def ExitMainLoop(self) -> None:
82
+ """Signal the async MainLoop to exit on its next iteration."""
83
+ self.exiting = True
84
+
85
+ def AsyncBind(
86
+ self,
87
+ event_binder: wx.PyEventBinder,
88
+ async_callback: CoroutineFn,
89
+ object: wx.Window,
90
+ source: wx.EvtHandler | None = None,
91
+ id: int = wx.ID_ANY,
92
+ id2: int = wx.ID_ANY,
93
+ ) -> None:
94
+ """Bind a coroutine to a wx Event.
95
+
96
+ When the bound wx object is destroyed, any running coroutine is
97
+ cancelled automatically via OnDestroy.
98
+
99
+ Raises:
100
+ Exception: If object is not a wx.Window or async_callback is not a coroutine.
101
+ """
102
+ if not isinstance(object, wx.Window):
103
+ raise Exception("object must be a wx.Window")
104
+ if not inspect.iscoroutinefunction(async_callback):
105
+ raise Exception("async_callback is not a coroutine function")
106
+ if object not in self.BoundObjects:
107
+ self.BoundObjects[object] = defaultdict(list)
108
+ object.Bind(
109
+ wx.EVT_WINDOW_DESTROY,
110
+ lambda event: self.OnDestroy(event, object),
111
+ object,
112
+ )
113
+ self.BoundObjects[object][event_binder.typeId].append(async_callback)
114
+ object.Bind(
115
+ event_binder,
116
+ lambda event: StartCoroutine(async_callback(event.Clone()), object),
117
+ source=source,
118
+ id=id,
119
+ id2=id2,
120
+ )
121
+
122
+ def StartCoroutine(
123
+ self, coroutine: Coroutine[Any, Any, Any] | CoroutineFn, obj: wx.Window
124
+ ) -> asyncio.Task[Any]:
125
+ """Start and attach a coroutine to a wx object.
126
+
127
+ When the object is destroyed, the coroutine is cancelled automatically.
128
+ Returns the asyncio.Task for the running coroutine.
129
+
130
+ Raises:
131
+ Exception: If obj is not a wx.Window.
132
+ """
133
+ if not isinstance(obj, wx.Window):
134
+ raise Exception("obj must be a wx.Window")
135
+ if inspect.iscoroutinefunction(coroutine):
136
+ coroutine = coroutine()
137
+ if obj not in self.BoundObjects:
138
+ self.BoundObjects[obj] = defaultdict(list)
139
+ obj.Bind(
140
+ wx.EVT_WINDOW_DESTROY, lambda event: self.OnDestroy(event, obj), obj
141
+ )
142
+ task = asyncio.create_task(coroutine) # type: ignore[arg-type]
143
+ task.obj = obj # type: ignore[attr-defined]
144
+ task.add_done_callback(self.OnTaskCompleted)
145
+ self.RunningTasks[obj].add(task)
146
+ return task
147
+
148
+ def OnTaskCompleted(self, task: asyncio.Task[Any]) -> None:
149
+ """Handle completion of a tracked coroutine task.
150
+
151
+ Calls task.result() to surface any exception from the coroutine.
152
+ CancelledError is silenced because it's expected when a window is destroyed.
153
+ Other exceptions are surfaced via warnings.warn so cleanup still runs.
154
+ """
155
+ try:
156
+ task.result()
157
+ except CancelledError:
158
+ pass
159
+ except Exception as exc:
160
+ warnings.warn(f"Exception in async callback: {exc!r}", RuntimeWarning)
161
+ finally:
162
+ obj = getattr(task, "obj", None)
163
+ if obj is not None:
164
+ tasks = self.RunningTasks.get(obj)
165
+ if tasks is not None:
166
+ tasks.discard(task)
167
+ if not tasks:
168
+ del self.RunningTasks[obj]
169
+
170
+ def OnDestroy(self, event: wx.WindowDestroyEvent, obj: wx.Window) -> None:
171
+ """Cancel all running tasks for a window and clean up its bindings."""
172
+ tasks = list(self.RunningTasks.get(obj, set()))
173
+ for task in tasks:
174
+ if not task.done():
175
+ task.cancel()
176
+ if self.warn_on_cancel_callback:
177
+ warnings.warn("cancelling callback" + str(obj) + str(task))
178
+ del self.BoundObjects[obj]
179
+ if obj in self.RunningTasks:
180
+ del self.RunningTasks[obj]
181
+
182
+
183
+ def AsyncBind(
184
+ event: wx.PyEventBinder,
185
+ async_callback: CoroutineFn,
186
+ object: wx.Window,
187
+ source: wx.EvtHandler | None = None,
188
+ id: int = wx.ID_ANY,
189
+ id2: int = wx.ID_ANY,
190
+ ) -> None:
191
+ """Module-level convenience wrapper for WxAsyncApp.AsyncBind.
192
+
193
+ Raises:
194
+ Exception: If no WxAsyncApp instance exists.
195
+ """
196
+ app = wx.App.Get()
197
+ if not isinstance(app, WxAsyncApp):
198
+ raise Exception("Create a 'WxAsyncApp' first")
199
+ app.AsyncBind(event, async_callback, object, source=source, id=id, id2=id2)
200
+
201
+
202
+ def StartCoroutine(
203
+ coroutine: Coroutine[Any, Any, Any] | CoroutineFn, obj: wx.Window
204
+ ) -> asyncio.Task[Any]:
205
+ """Module-level convenience wrapper for WxAsyncApp.StartCoroutine.
206
+
207
+ Raises:
208
+ Exception: If no WxAsyncApp instance exists.
209
+ """
210
+ app = wx.App.Get()
211
+ if not isinstance(app, WxAsyncApp):
212
+ raise Exception("Create a 'WxAsyncApp' first")
213
+ return app.StartCoroutine(coroutine, obj)
214
+
215
+
216
+ async def ShowModalInExecutor(dialog: wx.Dialog) -> int:
217
+ """Show a modal OS dialog on the wx main thread and await its return code.
218
+
219
+ Schedules ``dialog.ShowModal()`` via ``wx.CallAfter`` so the GUI call always
220
+ runs on the wx main thread. The result is delivered through an
221
+ ``asyncio.Future``. The asyncio event loop blocks while the modal dialog
222
+ runs its nested wx event loop; this is expected single-threaded modal
223
+ behavior.
224
+
225
+ Required for dialogs like wx.FileDialog, wx.DirDialog, etc.
226
+ """
227
+ loop = asyncio.get_running_loop()
228
+ future: asyncio.Future[int] = loop.create_future()
229
+
230
+ def on_main_thread() -> None:
231
+ try:
232
+ result = dialog.ShowModal()
233
+ except Exception as exc:
234
+ future.set_exception(exc)
235
+ else:
236
+ future.set_result(result)
237
+
238
+ wx.CallAfter(on_main_thread)
239
+ return await future
240
+
241
+
242
+ async def AsyncShowDialog(dialog: wx.Dialog) -> int:
243
+ """Show a dialog in async modless mode and wait for its result.
244
+
245
+ Raises:
246
+ Exception: If the dialog type does not support modless display;
247
+ use AsyncShowDialogModal for those.
248
+ """
249
+ if not isinstance(
250
+ dialog,
251
+ (
252
+ wx.FileDialog,
253
+ wx.DirDialog,
254
+ wx.FontDialog,
255
+ wx.ColourDialog,
256
+ wx.MessageDialog,
257
+ ),
258
+ ):
259
+ closed = asyncio.Event()
260
+
261
+ def end_dialog(return_code: int) -> None:
262
+ dialog.SetReturnCode(return_code)
263
+ dialog.Hide()
264
+ closed.set()
265
+
266
+ async def on_button(event: wx.CommandEvent) -> None:
267
+ id = event.GetId()
268
+ if id == dialog.GetAffirmativeId():
269
+ if dialog.Validate() and dialog.TransferDataFromWindow():
270
+ end_dialog(id)
271
+ elif id == wx.ID_APPLY:
272
+ if dialog.Validate():
273
+ dialog.TransferDataFromWindow()
274
+ elif id == dialog.GetEscapeId() or (
275
+ id == wx.ID_CANCEL and dialog.GetEscapeId() == wx.ID_ANY
276
+ ):
277
+ end_dialog(wx.ID_CANCEL)
278
+ else:
279
+ event.Skip()
280
+
281
+ async def on_close(event: wx.CloseEvent) -> None:
282
+ closed.set()
283
+ dialog.Hide()
284
+
285
+ AsyncBind(wx.EVT_CLOSE, on_close, dialog)
286
+ AsyncBind(wx.EVT_BUTTON, on_button, dialog)
287
+ dialog.Show()
288
+ await closed.wait()
289
+ return dialog.GetReturnCode()
290
+
291
+ raise Exception(
292
+ "This type of dialog cannot be shown modless, please use 'AsyncShowDialogModal'"
293
+ )
294
+
295
+
296
+ async def AsyncShowDialogModal(dialog: wx.Dialog) -> int:
297
+ """Show a dialog in modal mode.
298
+
299
+ OS-level dialogs (FileDialog, DirDialog, etc.) are run via ShowModalInExecutor.
300
+ Other dialogs disable parent frames, show modless, and re-enable on close.
301
+ """
302
+ if isinstance(
303
+ dialog,
304
+ (
305
+ wx.html.HtmlHelpDialog,
306
+ wx.FileDialog,
307
+ wx.DirDialog,
308
+ wx.FontDialog,
309
+ wx.ColourDialog,
310
+ wx.MessageDialog,
311
+ ),
312
+ ):
313
+ return await ShowModalInExecutor(dialog)
314
+
315
+ frames = set(wx.GetTopLevelWindows()) - {dialog}
316
+ states = {frame: frame.IsEnabled() for frame in frames}
317
+ try:
318
+ for frame in frames:
319
+ frame.Disable()
320
+ return await AsyncShowDialog(dialog)
321
+ finally:
322
+ for frame in frames:
323
+ frame.Enable(states[frame])
324
+ parent = dialog.GetParent()
325
+ if parent:
326
+ parent.SetFocus()
File without changes