Using Python’s async/await with PyGObject has made my code synchronous again. Wy?

Ok, so here’s the beat: I’m developing a Rhythmbox plugin that allows me to copy songs of a playlist from a place to another.

I used Gio.File.copy_async to copy the files. Here’s what I did at first (the code is actually more complex but stick with me):

files = []
pending = []

def copy_file(file: Gio.File, cancellable: Gio.Cancellable):
    destination_path = ""  # Compute new path

    cancellable.connect(on_cancel)

    self.__file.copy_async(
        Gio.File.new_for_path(destination_path),
        Gio.FileCopyFlags.ALL_METADATA
        | Gio.FileCopyFlags.NOFOLLOW_SYMLINKS
        | Gio.FileCopyFlags.OVERWRITE,
        GLib.PRIORITY_DEFAULT,
        cancellable,
        on_progress,
        (),
        on_file_copied,
        None,
    )

def on_cancel():
  ...

def on_progress(self, current_num_bytes: int, total_num_bytes: int):
  ...

def on_file_copied(self, file: Gio.File, res: Gio.AsyncResult, _):
  pending.remove(file)
  
  if len(pending) == 0:
    on_batch_done()
  else
    ...
    
def on_batch_done():
  # Do things after files have been copied
  ...

def copy_files():
  files = []
  cancellable = Gio.Cancallable()
  for file in files:
    copy_file(file, cancellable)

At first it was managable but, as code grew more complex, using Gio’s *_async functions turned the whole thing into a callback hell and made the code more difficult to reason about.

So I decided to use Python’s async/await to turn callbacks into awaitable coroutines like I’m used to do in Kotlin, now:

class TransfertTask(GObject.Object):
    def __init__(
        self,
        destination: str,
        file: Gio.File,
        cancellable: Gio.Cancellable,
        loop: AbstractEventLoop,
    ):
        ...
        super().__init__()

    def start(self):
        self.__cancellable.connect(self.__on_cancel)

        self.__file.copy_async(
            Gio.File.new_for_path(self.destination),
            Gio.FileCopyFlags.ALL_METADATA
            | Gio.FileCopyFlags.NOFOLLOW_SYMLINKS
            | Gio.FileCopyFlags.OVERWRITE,
            GLib.PRIORITY_DEFAULT,
            self.__cancellable,
            self.__on_progress,
            (),
            self.__on_file_copied,
            None,
        )

        return self.__future

    def __on_progress(self, current_num_bytes: int, total_num_bytes: int):
      ...

    def __on_cancel(self):
        self.__future.cancel()

    def __on_file_copied(self, file: Gio.File, res: Gio.AsyncResult, _):
        async def set_future_result():
            self.__future.set_result(self)

        self.__finished = True
        run_coroutine_threadsafe(set_future_result(), self.__loop)
        try:
            file.copy_finish(res)
        except GLib.Error as e:
            self.error = e
  
async def copy_files():
  cancellable = Gio.Cancellable()
  loop = get_running_loop()
  
  await gather(*[
    TransfertTask("...", file, cancellable, loop).start()
    for file in files
  ])
  
def start():
  asyncio.run(copy_files())

Thing is, now the code blocks the UI until the files a copied. In order to recover async processing, I have to launch the main coroutine in a seperate thread like so:

def start():
  loop = get_event_loop()
  thr = Thread(target=loop.run_forever)
  thr.daemon = True
  thr.start()
  run_coroutine_threadsafe(copy_files(), loop)

I’m very new to asyncio so there’s a few things I still don’t quite understand. I would expect await or event asyncio.run() to be blocking until the coroutine has fininshed running. But I couldn’t find anywhere on the internet a way to tell Python "just launch this coroutine and move on, I don’t care about the result". Maybe Python can’t do that and I’m still too much thinking like I’m writing Kotlin.

Can someone tell me what I’m doing wrong?

Source: Python Questions

LEAVE A COMMENT