Metaclasses & PyGObject

I recently ran into a long-standing bug in the Kiwi plugin for Gazpacho.

I have this class in Kiwi: (simplified for explanation purposes)

class KiwiEntry(gtk.Entry):
  gproperty('data_type', object)
  def __init__(self, data_type):
     gtk.Entry.__init__(self)
     self.set_property('data_type', data_type)

This worked absolutely fine when you construct the object by calling the constructor from your python code.
However, Gazpacho and also Glade-3 uses gobject.new() to construct objects, which uses an alternative construction path.

When using gobject.new() to construct the KiwiEntry, an exception would be raised that said that data_type was not a property of the object!

After a few hours of digging in Gazpacho and Kiwi I found out that the underlying bug was actually in PyGObject, the python bindings for GObject. It turned out that the GObject subclasses constructed using gobject.new was not properly created during the constructor phase.

Changing the code to set the property immediately after the constructor would magically solve the bug.

First I tried to solve the bug by using gobject.idle_add. It would actually work but it did not really satisfy me since it depends on the mainloop running. Not too much of a deal actually, since you need a mainloop to use the entry subclass anyway. However, I knew that this would cause hard to debug problems in the future.

The solution of course is to use a metaclass!
A metaclass is a class of a class, which allows you to modify how a class behaves. For instance, you can add a piece of code which is ran when the class is constructed if you like. Or in my case, add a hook which is called after the normal construct has been called.

class MyMeta(gobject.GObjectMeta):
    def __call__(self, *args, **kwargs):
        instance = super(MyMeta, self).__call__(*args, **kwargs)
        instance.__post_init__()
        return instance

This metaclass overrides the __call__ method which is the code being run when you put two parenthesis after the class:

  class()

To use this metaclass for an object, just define the __metaclass__ class attribute in the subclass(es) you wish to use it in:

class KiwiEntry(gtk.Entry):
  __metaclass__ = MyMeta

  def __init__(self, data_type=None):
     gtk.Entry.__init__(self)
     self._data_type = data_type

  def __post_init__(self):
     self.set_property('data-type', self._data_type)

A Metaclass is a very powerful concept, it should be used with caution. It’s very easy to abuse them and make code not only hard to understand, but almost impossible to debug.

For more information about metaclasses, refer to the Unifying types and classes in Python 2.2 which contains a quite technical explanation.

This entry was posted in GNOME, PyGTK, Python. Bookmark the permalink.