-
-
Notifications
You must be signed in to change notification settings - Fork 684
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
Document passing arguments to event handlers #2099
Comments
The request as you've made it doesn't entirely make sense. Event handlers have a very specific prototype:
There's no capacity to pass in arguments to a handler; and background tasks are no different to any other handler. This stands to reason - where do the extra arguments come from? You're not in control of invoking the handler, so there's no way to pass additional arguments in when it is invoked. However - that's not what you're describing here. What you're describing is providing additional context at the time the background handler (or any event handler for that matter) is defined. This you definitely can do. One approach is to use a factory method:
then invoking:
Calling An alternative spelling of the same approach is
Alternatively, if what you want to do is schedule a coroutine... then you can do that with raw asyncio primitives, without using
Toga's event loop is fully integrated with Python's event loop, so all of Python's event loop handling APIs will work, with he caveat that an event loop is already running. The proposed syntax of This is a question that comes up a lot, so I'm going to convert this to a documentation ticket. We clearly need a topic guide to explain how to get extra context into a handler, and the other out-of-the-box options that exist for doing background work. |
@freakboy3742: At one point you use the name And I think maybe some renamed or additional methods would be a good idea, because what the method accepts isn't a |
I think this is the point worth documenting the most. All other gui toolkits require you to use their “schedule” or “schedule later” functionality to have calls to expensive functions play well with their rendering thread, which is probably we users look to your API on how to do that.
Say a use case is this: you have a set of buttons, all of which can cause an “intensive” task (let’s say a download so it’s IO-intensive)
Now you want to start the download, so options are:
Roughly the missing piece of the puzzle for me ws that you can just do the second with |
🤦
That's a good point. I'm not opposed to a rename in the interests of clarity; what did you have in mind in terms of renames and/or additional methods? Do you think my accidental flub of |
Agreed that this is what most GUI frameworks do; however, most GUI frameworks don't come from languages that have native async handling. :-)
I'm extremely familiar with the use case - in fact, just last week I gave a presentation at PyCon AU in which "you want to download something" is the exact example that I use for a long lived task blocking the GUI event loop. However your description of the mechanics of a download handler misses my point - when you say "passing an argument into a handler", do you mean "at the point it is constructed", or "at the point it is invoked". Your theoretical example of To make the point more obvious: let's say the download URL is coming from a text input - are you binding the value of the text input when you queue the download, or reading the value that is there when the download starts? The two values need not be the same. If the case of "queue this URL for download", you almost certainly want to bind the value at time of queuing; but in the case of "start a download when the button is pressed", you want to read the value of the text input at the time the button is pressed, not use a pre-bound value. All this by way of saying - there's a reason that the argument to Toga handlers is a reference to a function, not a co-routine; and if you want to schedule a co-routine (which is an invoked async function) for execution, (Also, FWIW: You've described a moderately complex queue-based download system; a much simpler approach would be to use an async request API such as |
In this example, arguments are passed in when the coroutine function is invoked, which is when the coroutine is created. I’m sorry the python terminology is a bit confusing. I’ve written out the example a bit more below to see if that makes more sense?
Definitely something that can be done in the button click handler.
The request is not what is making up the complexity, but handling starting downloads, awaiting them, properly handling errors -- this is all boilerplate stuff and shouldn’t have to be written out every time. This is the coroutine-as-task model I would like to use def App(toga.App):
def startup(self):
self.textbox = toga.TextInput()
self.button = toga.Button('text=Download!', on_press=self.click_handler)
# window setup etc.
self.client = httpx.AsyncClient()
def click_handler(self, widget):
url = self.textbox.value
self.add_background_task(self.download(url)) # here we start 1 coroutine per download. NB. asyncio idiom that does not work with toga, use asyncio.create_task() instead.
async def download(self, url):
async with self.client.stream('GET', url) as response:
async for chunk in response.aiter_bytes():
process(chunk) This is the handler model as I understand it (?) def App(toga.App):
def startup(self):
self.textbox = toga.TextInput()
self.button = toga.Button('text=Download!', on_press=self.click_handler)
# window setup etc.
self.client = httpx.AsyncClient()
self.queue = asyncio.Queue()
self.add_background_task(App.downloader) # here we start 1 unique handler in startup()
def click_handler(self, widget):
url = self.textbox.value
self.queue.put_nowait(url) # provision queue size well or risk QueueEmpty! Can’t async put as we are in a synchronous callback.
async def downloader(self):
while True:
url = await self.queue.get()
try:
async with self.client.stream('GET', url) as response:
async for chunk in response.aiter_bytes():
process(chunk)
except:
# If we don’t do this ourselves, we prevent all future downloads from working
pass # probably print to stderr or something
finally:
self.queue.task_done() Note that the second version does not support concurrent downloads. To do that, you’d have to create futures out of every download (which is what If I may suggest, one good way of documenting this would be to print an elightening error message when calling |
Thanks for pointing out In fact, since |
That would definitely clear up the confusion :) |
Deprecating |
Maybe we should deprecate that too, now that Python has built-in async support (#2721). |
Yeah - that's probably the best option. Clearly documenting |
One related data point: #1415 is a request for "run later" capability. This can be achieved with I'm not sure if this means we should have (or, I guess, the third option: an opportunity to contribute upstream to CPython and add an asyncio.call_later() shim...) |
Now that the event loop is available as a property on the App object, this can be simplified to We should also have an example of how to integrate blocking functions which unavoidably must be run on a separate thread. In the course of answering some StackOverflow questions (1, 2), I've come up with a pretty clean way of doing that: async def my_task(self):
while whatever:
value = await self.loop.run_in_executor(
None, some_slow_function, arg1, arg2, ...
)
self.label.text = value Python 3.9 adds a Related: |
#2720 deprecated |
Summary by @freakboy3742
We are regularly asked either (a) how to "pass in arguments to event handlers", or (b) for a feature addition to do the same. This isn't something that needs additional functionality - it just needs a better understanding of how Python can be used.
We should add a topic guide describing how to pass in additional details to an event handler, including:
add_background_task
for background work.Originally framed in the form of a feature request, asking for the ability to pass in coroutines to add_background_task; details follow. The comments following the ticket give a starting point for a discussion in the topic guide.
Original post by @Cimbali
What is the problem or limitation you are having?
Currently
add_background_task
accepts generators and coroutine functions (i.e. functions that return a coroutine). This makes passing an asynchronous function that takes arguments quite awkward.Suppose I have an async function (“coroutine function”) that takes some arguments:
With asyncio, the idiom is call the function, have it return a coroutine, and let asyncio take care of running it:
In toga, the idiom requires toga to call the function, which means we are now fixing the prototype of the function:
Describe the solution you'd like
If
add_background_function
supported passing in coroutines, we could do:Describe alternatives you've considered
If I want to pass several arguments to an async function run in the background, the most concise I can do is:
Which in more complex code becomes quite annoying, and means a function closure for every call, plus an additional
await
.Additional context
Note you also can’t do:
This also returns a coroutine which is awaitable (and
asyncio.run(wrapper(app))
woulc work as expected) but the wrapper isn’t decorated as a coroutine function.The text was updated successfully, but these errors were encountered: