I’ve been struggling with this too. It seems like python bindings for Gio.Task
do not work as expected. Anyway, I’ve managed to run background tasks using Gio.Task
now. This is what I understand so far.
-
Both
make_lunch_async
andmake_lunch_finish
should be part of the same class that definesmake_lunch
i.e.Cook
class. -
A class that defines async functions and uses
Gio.Task
(in this caseCook
class) should derive fromGObject.Object
. -
Probably the name
make_lunch_async_callback
should not be used for a function because there are two different functions called bymake_lunch_async
that do very different things-
One is called to prepare and run
make_lunch
in a thread. I’ve named this function_make_lunch_thread_callback
.- This function should be private i.e. its name should start with an underscore.
- This should be part of the same class which defines
make_lunch_async
(in this caseCook
class). - This function should not do any UI changes.
-
The other is called when previously mentioned function (
_make_lunch_thread_callback
) returns. I’ve named this functionon_make_lunch_finished
.- This is primarily used to update the UI according to the outcome.
- This should be part of the same class in which we call
make_lunch_async
(in this caseAppWindow
class).
-
-
Function
make_lunch_finish
is responsible for retrieving the return value from our task. -
The word
result
should not be used for outcome/return_value ofmake_lunch
. Using it can be confusing since it is used by the documentation for function arguments of types that implementGio.AsyncResult
interface.Gio.Task
also implements it.So, I use
result
as argument name only for ourGio.Task
instance and only when it is being used as aGio.AsyncResult
.And I use only the word
outcome
to represent the outcome ofmake_lunch
.
See code comments for more information.
#!/bin/env python3
import random
import time
import sys
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gio, Gtk
# VIEWS
class App(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(
self,
application_id="org.example.app",
flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
# Show default application window.
window = AppWindow(self)
self.add_window(window)
window.show_all()
@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
__gtype_name__ = "AppWindow"
start_button = Gtk.Template.Child("start-button")
operation_box = Gtk.Template.Child("operation-box")
progress_box = Gtk.Template.Child("progress-box")
spinner = Gtk.Template.Child("spinner")
success_box = Gtk.Template.Child("success-box")
success_label = Gtk.Template.Child("success-label")
failure_box = Gtk.Template.Child("failure-box")
failure_label = Gtk.Template.Child("failure-label")
back_button = Gtk.Template.Child("back-button")
def __init__(self, app):
super().__init__()
self.set_application(app)
@Gtk.Template.Callback()
def on_start_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(False)
self.progress_box.set_visible(True)
self.spinner.start()
cook = Cook()
cook.make_lunch_async(self.on_make_lunch_finished, ("rice", "lentils", "carrots"))
@Gtk.Template.Callback()
def on_back_button_clicked(self, button):
"""Handle the BUTTON's clicked signal."""
self.operation_box.set_visible(True)
self.success_box.set_visible(False)
self.failure_box.set_visible(False)
button.set_visible(False)
def on_make_lunch_finished(self, source_object, result, user_data):
'''Get outcome from make_lunch_async and call show_outcome'''
cook = source_object
outcome = cook.make_lunch_finish(result)
if outcome != -1:
self.show_outcome(outcome)
self.spinner.stop()
def show_outcome(self, outcome):
"""Update application according to the outcome."""
self.progress_box.set_visible(False)
self.back_button.set_visible(True)
if isinstance(outcome, Plate):
message = "Lunch is ready: {}".format(outcome)
self.success_label.set_text(message)
self.success_box.set_visible(True)
else:
message = outcome.get("Error")
self.failure_label.set_text(message)
self.failure_box.set_visible(True)
# MODELS
class Plate():
def __init__(self, ingredients):
self.ingredients = ingredients
def __str__(self):
return ", ".join(self.ingredients)
class Cook(GObject.Object):
def __init__ (self):
super().__init__()
# Required to hold actual task data since we cannot
# set task data directly.
self.pool = {}
def make_lunch(self, *ingredients):
time.sleep(5)
outcomes = [
Plate(ingredients),
{"Error": "Lunch is burned!!"}
]
return random.choice(outcomes)
def make_lunch_async (self, callback, ingredients):
'''Make lunch asynchronously (in the background)'''
task = Gio.Task.new(self, None, callback, None)
task.set_return_on_cancel(False)
# Since we cannot set task data to anything accept None or an integer, we
# set task data to id of our actual data (ingredients) and store the actual
# data in an instance-wide dictionary (self.pool) with key being the id of our data.
# Also, we need to remove our data from the dictionary when we're done with it.
ingredients_id = id(ingredients)
task.set_task_data(ingredients_id, lambda x: self.pool.pop(ingredients_id))
self.pool[ingredients_id] = ingredients
task.run_in_thread(self._make_lunch_thread_callback)
# Below function is only for internal use. So, it should be private
# i.e. it's name should start with an underscore
def _make_lunch_thread_callback (self, task, source_object, task_data, cancellable):
'''Called by make_lunch_async to make lunch in a thread'''
if task.return_error_if_cancelled():
return
# Value of argument 'task_data' is always None for some reason.
# So, we need to get task_data using task.get_task_data() method.
task_data = task.get_task_data()
# Task data is just an id of the actual data. So, we need to get
# the actual data from our instance-wide dictionary
ingredients = self.pool[task_data]
outcome = self.make_lunch(*ingredients)
task.return_value(outcome)
def make_lunch_finish (self, result):
'''Get outcome of making_lunch'''
# Note that Gio.Task.is_valid is a class method. So, no matter if
# you call it as Gio.Task.is_valid or as result.is_valid, you will
# have to give it `result` as the first argument
if not Gio.Task.is_valid(result, self):
return -1
return result.propagate_value().value
# RUN APP
if __name__ == "__main__":
app = App()
exit_status = app.run(sys.argv)
sys.exit(exit_status)