Making a Declarative GUI Implementation

Metaclasses for fun and profit

Europython 2016

Anders Hammarquist <iko@openend.se>
Henrik Bohre, Autolabel AB, http://autolabel.se
Mikael Schönenberg, OpenEnd AB, http://openend.se

Me

Python since 1.4

OpenEnd AB since 2001
Consulting and webapps
Autolabel AB since 2014
Labeling printers
Python, Databases, Email,
Domain-specific languages,
System integration, Embedded, etc

Introduction

Metaclasses
Inspiration on how to use for simplifying syntax (metaprogramming)
GTK+
How to describe a user interface
Why?
New UI for Autolabel printers

Why?

Shiny new UI for Autolabel printers1

images/Slider_Autolabel_D43-1.jpg

Settings UI

images/marker-settings.png

GTK+

GIMP Toolkit
Modern GUI toolkit
PyGTK
Python 2, GTK 2
GObject introspection

GTK 3, compatibility layer for GTK 2

Python wrapper that reads gi .typelib files to create Python classes

Goal: simple GUI definition

images/demo_window.png
class Top(Window):
  title='Hello world'

  class Group(VBox):

    class Label(Label):
      label = 'Hello World!'

    class HiButton(Button):
      class Label(Label):
        label = 'Hi!'

    class ByeButton(Button):
      class Label(Label):
        label = 'Bye!'

GTK3 python

class MyWindow(Gtk.Window):
  def __init__(self):
    Gtk.Window.__init__(self, title="Hello World")  #\n
    self.box = Gtk.VBox()
    self.add(self.box)  #\n
    self.label = Gtk.Label(label='Hello World!')
    self.box.add(self.label)  #\n
    self.hibutton = Gtk.Button()
    self.hilabel = Gtk.Label(label='Hi!')
    self.hibutton.add(self.hilabel)
    self.box.add(self.hibutton)  #\n
    self.byebutton = Gtk.Button()
    self.byelabel = Gtk.Label(label='Bye!')
    self.byebutton.add(self.byelabel)
    self.box.add(self.byebutton)

Metaclasses

images/Wizard_kingdoms.jpg


Metaclasses

What is a metaclass?

The class of the class

>>> class Foo(object): pass
>>> type(Foo)
<type 'type'>

>>> class mBar(type): pass
>>> class Bar(object):     # class Bar(metaclass=mBar)
>>>     __metaclass__ = mBar
>>> type(Bar)
<class '__main__.mBar'>

What is a metaclass?

The metaclass lets you customize the creation of the class, just as the class customizes the creation of objects.

images/metaclass.svg

Metametaclasses?

Metaclass methods

__new__(mcls, name, bases, namespace)
Called to instantiate class. Lets you mangle namespace etc.
Do weird stuff by not returning an instance of the metaclass!
__init__(cls, name, bases, namespace)
Called on a newly instantiated class. Lets you add to and modify the class.
__prepare__(name, bases, **kwargs)
Python3 only. Called to set up class __dict__. Useful when order is important. Use @staticmethod (or @classmethod) for unittesting.

Python3 adds keyword arguments to class definition that are passed to the metaclass methods.

Metaclass methods

Metaclass methods available on class, but not its instances.

class mFoo(type):
   def foo(self, arg):
      pass

class Foo(object):
   __metaclass__ = mFoo

Foo.foo(42)  # OK, self is Foo

foo = Foo()
foo.foo(42)  # AttributeError

Metaclass wrapup

class Foo(object):
   __metaclass__ = mFoo  # type if not specified
   foo = 42

   def bar(self, arg):
      pass

class is just syntactic sugar for instantiating the metaclass!

def bar(self, arg):
   pass

Foo = mFoo('Foo', (object,), { 'foo': 42, 'bar': bar })

Metaclass or not comparison

class Top(Window):
  title='Hello world'

  class Group(VBox):
    class Label(Label):
      label = 'Hello World!'
##################################################
class MyWindow(Gtk.Window):
  def __init__(self):
    Gtk.Window.__init__(self, title="Hello World")

    self.box = Gtk.VBox()
    self.add(self.box)

    self.label = Gtk.Label(label='Hello World!')
    self.box.add(self.label)

Walkthrough



Walkthrough

Attribute order

Figuring out attribute order (Python 2)

GTK quirk
GObject has a metaclass, so our metaclass must subclass its metaclass

Attribute order metaclass

class mBaseWidget(type(GObject.GObject)):

  attributes = []

  def __new__(cls, name, bases, namespace):
    ordered = []
    objects = set(id(o) for o in namespace.itervalues())

    for o in cls.attributes:
      if id(o) in objects:
        ordered.append(o)
    for o in ordered: cls.attributes.remove(o) # iter

    namespace['_attributes_ordered'] = ordered
    super(mBaseWidget, self).__new__(cls, name, bases, namespace)

Attribute order example

class BaseWidget(GObjcet.GObject):
  __metaclass__ = mBaseWidget

class Window(BaseWidget, Gtk.Window): pass

class VBox(BaseWidget, Gtk.VBox): pass

class Top(Window):
  class VBox(VBox):
    class ...:

  class VBox2(VBox):
    class ...:

# Top._attributes_ordered == [ Top.VBox. Top.VBox2 ]

Widget properties

Setting properties on the widget
  • GTK properties (title)
  • Properties on our own complex widgets
class Top(Window):
  title = 'Hello!'

class Bar(FooWidget):
  foo = 42  # call FooWidget.foo.__set__ with 42 after __init__

Needs support from both metaclass and base class.

Can't use Property on metaclass, as it's not instantiated yet.

Widget properties metaclass

class mBaseWidget(type(GObject.GObject)):
  def __new__(...):
    ...
    defaults = namespace.setdefault('__defaults__', {})
    for key, val in namespace.items()
      if hasattr(val, '__set__'):
        defaults[key] = None  # overwrite any earlier default
      else:
        for b in bases:
          if (hasattr(getattr(b, key, None), '__set__') # setter
            or not key.startswith('_') and
            hasattr(getattr(b, 'props', None), key)): # gtk props
            defaults[key] = val
            del namespace[key]

Widget properties base class

class BaseWidget(GObject.GObject):
  def __init__(self, **kwargs):
      self.gtk_params = kwargs.pop('gtk_params', self.gtk_params)
      super(BaseWidget, self).__init__(**self.gtk_params)

      defaults = self.__defaults__  # set by metaclass
      for cls in reversed(self.__class__.__mro__):
        if hasattr(cls, '__defaults__'):
          defaults.update(cls.__defaults__)
      defaults.update(kwargs)

      for k, v in defaults.iteritems():
        if hasattr(self.props, k): setattr(self.props, k, v)
        else: setattr(self, k, v)

Aside: __mro__

__mro__
the ordered list of classes to look for attributes in.
(<type 'str'>, <type 'basestring'>, <type 'object'>)

The first hit is the one you're supposed to use.

super() uses __mro__ to find the proper base class.

Sometimes easier to overwrite, so here I walk it in reverse.

How it is calculated is explained in

Instantiating widget tree

class Top(Window):
   class Group(VBox):
      class Title(Label):
         label = 'Hello'

top = Top()

And then what? top.Group will still be the class

Iterate through the _attributes_ordered and instantiate. A job for the base class.

Widget __init__ may need to do additional things, such as packing subwidgets.

Instantiating widget base class

class BaseWidget(...):
  def __init__(...):
    ...
    objdict = {}

    for cls in self.__class__.__mro__:
      objdict.update(dict((v,k) for (k,v) in cls.__dict__.iteritems()

    for cls in reversed(self.__class__.__mro___):
      for o in getattr(cls, '_attributes_ordered', []):
        obj = o(parent_widget=self)
        k = objdict.get(o)
        if k: setattr(self, k, obj)

    self._attributes_ordered = ordered

GTK Mixins

GTK does not support multiple inheritance
Stuff can break in strange and mysterious ways if you try
But we wanted mixins!

Do stuff under the hood in the metaclass!

  • If we have more than one base class, insert an intermediate base class that just inherits the bases. This is to work around the fact that GTK will only look for certain magic attributes in the first base class.
  • Merge in __gsignals__ and __gproperties__ attributes from bases.
  • Make sure mixin classes and BaseWidget are NOT added to GTK type registry by overloading _type_register in metaclass

GTK Mixins metaclass

class mBaseWidget(...):
  def __init__(...):
    ...
    if len(bases) > 1:
      gtype = None
      for base in bases:
        if hasattr(base, '__gtype__'):
          # if more than one, and it is not the base GObject, fail
          gtype = base
      if gtype:
        ns2 = { '__doc__': 'Intermediate class' }
        base = super(mBaseWidget, cls).__new__(name, bases, ns2)
        bases = (base,)

GTK signals & properties

        # continues from last page
        gsignals = {}; knows_gsignals = {}

        for bcls in reversed(base.__mro__):
          gsignals.update(bcls.__gsignals__)
          if issubclass(bcls, GObject.GObject):
            known_gsignals.update(GSignal.signal_list_names(bcls)

        for sig in known_gsignals:
          del gsignals[sig]

        namespace['__gsignals__'] = gsignals
        # gproperties are handled similarly

Summary

Metaclasses
  • How classes are instantiated
  • Customize it
Inspiration
  • Go turn Python into the language you want!
  • Code to play with is Python2, 2to3 should turn it into Python3

You are now a wizard

Q&A

⸘‽

Code and slides