Skip to content

Commit 8530026

Browse files
authored
Deprecate use of generators as handlers. (beeware#3158)
1 parent 4de3491 commit 8530026

File tree

8 files changed

+71
-44
lines changed

8 files changed

+71
-44
lines changed

changes/2721.removal.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The use of generators as event handlers has been deprecated. Any generator-based event handler can be converted into an asynchronous co-routine by converting the handler to ``async def``, and using ``await asyncio.sleep(t)`` in place of ``yield t`` (for some sleep interval ``t``).

core/src/toga/handlers.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,18 @@ async def long_running_task(
3939
generator: HandlerGeneratorReturnT[object],
4040
cleanup: HandlerSyncT | None,
4141
) -> object | None:
42-
"""Run a generator as an asynchronous coroutine."""
42+
"""Run a generator as an asynchronous coroutine.
43+
44+
**DEPRECATED** - use async co-routines instead of generators.
45+
"""
46+
######################################################################
47+
# 2025-02: Deprecated in 0.5.0
48+
######################################################################
49+
warnings.warn(
50+
"Use of generators for async handlers has been deprecated; convert "
51+
"the handler to an async co-routine that uses `asyncio.sleep()`.",
52+
DeprecationWarning,
53+
)
4354
try:
4455
try:
4556
while True:

core/tests/test_handlers.py

+45-17
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ def handler(*args, **kwargs):
179179
)
180180

181181

182+
######################################################################
183+
# 2025-02: Generator handlers deprecated in 0.5.0
184+
######################################################################
185+
186+
182187
def test_generator_handler(event_loop):
183188
"""A generator can be used as a handler."""
184189
obj = Mock()
@@ -198,10 +203,15 @@ def handler(*args, **kwargs):
198203
# Raw handler is the original generator
199204
assert wrapped._raw == handler
200205

201-
# Invoke the handler, and run until it is complete.
202-
assert (
203-
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4)) == 42
204-
)
206+
# Invoke the handler, and run until it is complete. Raises a deprecation warning.
207+
with pytest.warns(
208+
DeprecationWarning,
209+
match=r"Use of generators for async handlers has been deprecated;",
210+
):
211+
assert (
212+
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4))
213+
== 42
214+
)
205215

206216
# Handler arguments are as expected.
207217
assert handler_call == {
@@ -228,11 +238,16 @@ def handler(*args, **kwargs):
228238
# Raw handler is the original generator
229239
assert wrapped._raw == handler
230240

231-
# Invoke the handler; return value is None due to exception
232-
assert (
233-
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4))
234-
is None
235-
)
241+
# Invoke the handler; raises a deprecation warning, return value is None due to
242+
# exception.
243+
with pytest.warns(
244+
DeprecationWarning,
245+
match=r"Use of generators for async handlers has been deprecated;",
246+
):
247+
assert (
248+
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4))
249+
is None
250+
)
236251

237252
# Handler arguments are as expected.
238253
assert handler_call == {
@@ -267,10 +282,15 @@ def handler(*args, **kwargs):
267282
# Raw handler is the original generator
268283
assert wrapped._raw == handler
269284

270-
# Invoke the handler
271-
assert (
272-
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4)) == 42
273-
)
285+
# Invoke the handler; raises a deprecation warning
286+
with pytest.warns(
287+
DeprecationWarning,
288+
match=r"Use of generators for async handlers has been deprecated;",
289+
):
290+
assert (
291+
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4))
292+
== 42
293+
)
274294

275295
# Handler arguments are as expected.
276296
assert handler_call == {
@@ -304,10 +324,15 @@ def handler(*args, **kwargs):
304324
# Raw handler is the original generator
305325
assert wrapped._raw == handler
306326

307-
# Invoke the handler; error in cleanup is swallowed
308-
assert (
309-
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4)) == 42
310-
)
327+
# Invoke the handler; raises a deprecation warning, error in cleanup is swallowed
328+
with pytest.warns(
329+
DeprecationWarning,
330+
match=r"Use of generators for async handlers has been deprecated;",
331+
):
332+
assert (
333+
event_loop.run_until_complete(wrapped("arg1", "arg2", kwarg1=3, kwarg2=4))
334+
== 42
335+
)
311336

312337
# Handler arguments are as expected.
313338
assert handler_call == {
@@ -327,6 +352,9 @@ def handler(*args, **kwargs):
327352
)
328353

329354

355+
######################################################################
356+
357+
330358
def test_coroutine_handler(event_loop):
331359
"""A coroutine can be used as a handler."""
332360
obj = Mock()

demo/toga_demo/app.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
import toga
24
from toga.constants import COLUMN
35
from toga.style import Pack
@@ -82,10 +84,10 @@ def startup(self):
8284
# Show the main window
8385
self.main_window.show()
8486

85-
def button_handler(self, widget):
87+
async def button_handler(self, widget):
8688
print("button press")
8789
for i in range(0, 10):
88-
yield 1
90+
await asyncio.sleep(1)
8991
print("still running... (iteration %s)" % i)
9092

9193
def action1(self, widget):

examples/handlers/handlers/app.py

-18
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ def do_clear(self, widget, **kwargs):
1515
self.on_running_label.text = "Ready."
1616
self.background_label.text = "Ready."
1717
self.function_label.text = "Ready."
18-
self.generator_label.text = "Ready."
1918
self.async_label.text = "Ready."
2019

2120
def do_function(self, widget, **kwargs):
@@ -25,17 +24,6 @@ def do_function(self, widget, **kwargs):
2524
random.randint(0, 100)
2625
)
2726

28-
def do_generator(self, widget, **kwargs):
29-
"""A generator-based handler."""
30-
# The generator yields a number; that number is the number of seconds
31-
# to yield to the main event loop before processing is resumed.
32-
widget.enabled = False
33-
for i in range(1, 10):
34-
self.generator_label.text = f"Iteration {i}"
35-
yield 1
36-
self.generator_label.text = "Ready."
37-
widget.enabled = True
38-
3927
async def do_async(self, widget, **kwargs):
4028
"""An async handler."""
4129
# This handler is integrated with the main event loop; every call to
@@ -81,7 +69,6 @@ def startup(self):
8169
self.on_running_label = toga.Label("Ready.", style=Pack(margin=10))
8270
self.background_label = toga.Label("Ready.", style=Pack(margin=10))
8371
self.function_label = toga.Label("Ready.", style=Pack(margin=10))
84-
self.generator_label = toga.Label("Ready.", style=Pack(margin=10))
8572
self.async_label = toga.Label("Ready.", style=Pack(margin=10))
8673
self.web_label = toga.Label("Ready.", style=Pack(margin=10))
8774

@@ -94,9 +81,6 @@ def startup(self):
9481
btn_function = toga.Button(
9582
"Function callback", on_press=self.do_function, style=btn_style
9683
)
97-
btn_generator = toga.Button(
98-
"Generator callback", on_press=self.do_generator, style=btn_style
99-
)
10084
btn_async = toga.Button(
10185
"Async callback", on_press=self.do_async, style=btn_style
10286
)
@@ -112,8 +96,6 @@ def startup(self):
11296
self.background_label,
11397
btn_function,
11498
self.function_label,
115-
btn_generator,
116-
self.generator_label,
11799
btn_async,
118100
self.async_label,
119101
btn_web,

examples/textinput/textinput/app.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from string import ascii_lowercase, ascii_uppercase, digits
23

34
import toga
@@ -10,7 +11,7 @@
1011

1112
class TextInputApp(toga.App):
1213
# Button callback functions
13-
def do_extract_values(self, widget, **kwargs):
14+
async def do_extract_values(self, widget, **kwargs):
1415
# Disable all the text inputs
1516
for input in self.inputs:
1617
input.enabled = False
@@ -38,7 +39,7 @@ def do_extract_values(self, widget, **kwargs):
3839
# Wait a few seconds
3940
for i in range(2, 0, -1):
4041
self.label.text = f"Counting down from {i}..."
41-
yield 1
42+
await asyncio.sleep(1)
4243
self.label.text = "Enter some values and press extract."
4344

4445
# Re-enable the inputs again.

examples/tutorial2/tutorial/app.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import asyncio
2+
13
import toga
24
from toga.style.pack import COLUMN, Pack
35

46

5-
def button_handler(widget):
7+
async def button_handler(widget):
68
print("button handler")
79
for i in range(0, 10):
810
print("hello", i)
9-
yield 1
11+
await asyncio.sleep(1)
1012
print("done", i)
1113

1214

examples/window/window/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ def do_window_state_normal(self, widget, **kwargs):
8686
def do_window_state_maximize(self, widget, **kwargs):
8787
self.main_window.state = WindowState.MAXIMIZED
8888

89-
def do_window_state_minimize(self, widget, **kwargs):
89+
async def do_window_state_minimize(self, widget, **kwargs):
9090
self.main_window.state = WindowState.MINIMIZED
9191
for i in range(5, 0, -1):
9292
print(f"Back in {i}...")
93-
yield 1
93+
await asyncio.sleep(1)
9494
self.main_window.state = WindowState.NORMAL
9595

9696
def do_window_state_full_screen(self, widget, **kwargs):

0 commit comments

Comments
 (0)