Create GObject.Closure from python

Hi,

I would like to use the Gio.File.copy_async API from python, with user data for the “finish” callback.

Since this glib MR language bindings are actually using Gio.File.copy_async_with_closures instead.
The python signature is now:

>>> Gio.File.copy_async.__doc__
'copy_async(self, destination:Gio.File, flags:Gio.FileCopyFlags, io_priority:int, cancellable:Gio.Cancellable=None, progress_callback_closure:GObject.Closure=None, ready_callback_closure:GObject.Closure)'

How can I create the ready_callback_closure? The GObject.Closure constructors new_object and new_simple seem specific to C (require to give the size of the struct…) so I can’t see any way to pass my user data…

Setting the struct member directly doesn’t work either:

>>> c = GObject.Closure()
>>> c.data = "foobar"
ValueError: Pointer arguments are restricted to integers, capsules, and None. See: https://bugzilla.gnome.org/show_bug.cgi?id=683599

Generally, when a parameter is of type GObject.Closure, that means a regular function in PyGObject.

Here’s a working example:

#! /usr/bin/python3

from gi.repository import GLib, Gio

def progress_cb(current_num_bytes, total_num_bytes):
    print(f'Copied {current_num_bytes} out of {total_num_bytes} bytes')

def ready_cb(file, res):
    print('Done copying')
    try:
        file.copy_finish(res)
    except GLib.Error as e:
        print('Error:', e)
    else:
        print('Success')

file1 = Gio.File.new_for_path('source.txt')
file2 = Gio.File.new_for_path('dest.txt')
file1.copy_async(file2, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, progress_cb, ready_cb)

loop = GLib.MainLoop()
loop.run()

Thanks, but my actual problem was about passing user data to the callbacks.

Something like this is not possible anymore:

def ready_cb(file, res, user_data):
    print(user_data)     # should print "my user data"
    ...

file1.copy_async(file2, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, None, ready_cb, "my user data")

I just figured out I can use a lambda instead:

def ready_cb(file, res, user_data):
    print(user_data)     # should print "my user data"
    ...

file1.copy_async(file2, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, None, lambda *args: ready_cb(*args, "my user data"))

but that’s less convenient, and doesn’t match the usual user-data passing used by other async APIs.

Instead of remapping the copy_async to copy_async_with_closures (which is an API break for language bindings), I think it would have been wiser to add a new copy_async_with_progress, just like copy_async but with the missing progress destroy notification, and map it for the bindings.
Then add a documentation warning to copy_async mentioning it’s only suitable in C when the progress callback is not used, otherwise copy_async_with_progress should be preferred.

Sorry, no: that’s not how anything works.

The g_file_copy_async() function is not bindable—as it shares the same user data argument across callbacks—so it cannot be used by language bindings. For that specific reason, g_file_copy_async_with_closures() was added to the C API, and it was marked to shadow g_file_copy_async() inside the introspection binary data that bindings like PyGObject use; since its introduction, Gio.File.copy_async() resolves to g_file_copy_async_with_closures(), and there’s nothing that PyGObject can do. It’s not an API break, so to speak, because Gio.File.copy_async() could not work in the past.

If you want to make the progress function optional, you’ll need an override inside PyGObject that shuffles around the arguments, something like:

from functools import partial

class File(Gio.File):
    def copy_async(self, dest, flags, priority, cancellable, ready_cb, ready_data, progress_cb, progress_data):
        ready = partial(ready_cb, ready_data)
        progress = partial(progress_cb, progress_data)
        return super().copy_async(dest, flags, priority, cancellable, ready, progress)

File = override(File)
__all__.append("File")

Or you can use partial yourself.

Yes, so either a lambda, or a local function:

def something():
    string = "my user data"

    def ready_cb(file, res):
        file.copy_finish(res)
        print(string)  # => my user data

    file1.copy_async(file2, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, None, ready_cb)

That’s how capturing variables always works in Python. Nothing GObject- or GClosure-specific about it.

What you might be saying is that for non-GClosure callbacks PyGObject also lets you pass a callback and an argument to it explicitly. IMO that’s a C-ism, and it’s always intended that in languages that have language-level support for closures, you’d use closures. But I can also see how that could be handy, and also indeed that creates an inconsistency between plain callbacks and GClosure.

Or another case is a bound method:

class Something:
    def do_something(self):
        file1 = Gio.File.new_for_path('source.txt')
        file2 = Gio.File.new_for_path('dest.txt')
        file1.copy_async(file2, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, self.progress_cb, self.ready_cb)

    def progress_cb(self, current_num_bytes, total_num_bytes):
        pass

    def ready_cb(self, file, res):
        file.copy_finish(res)
        self.something_else()