Home

Jan. 3, 2017, 3 min read

Python Signature-aware Interfaces

Python has no builtin concept of interfaces. In general this is no big deal, but I have found myself in situations where a slightly more enforced contract between classes would have been useful. Python comes with a similar thing called abstract base classes, which are meant to serve the purpose of an interface; however they only care about methods being implemented, but not their signature. In one of my tools I wanted to expose a single API, while under the hood use two different GUI libraries (which ever is available). Being consistent with method signatures was important to avoid those implementations drifting too far away from each other. I only pursued it to some degree as it felt like trying to force Python into something it just isn't, but maybe it is useful for someone else out there. Here is my approach to a signature-aware interface base class:

import inspect

class AbstractInterface(type):
    """A metaclass to build interfaces.

    Any derived class will be an interface that can be used as a meta
    class for concrete classes. The interface's methods (properties
    included) must be implemented with the same signature in the
    concrete class, otherwise its definition will fail.

    """
    def __new__(meta, clsname, bases, clsdict):
        metaname = meta.__name__
        metadict = meta.__dict__

        def prop(thing):
            return isinstance(thing, property)

        def callable_or_prop(thing):
            return callable(thing) or prop(thing)

        must_implement = [k for k, v in metadict.items()
                          if callable_or_prop(v)]
        for methodname in must_implement:
            methodident = "{clsname}.{methodname}()".format(**locals())
            try:
                clsmethod = clsdict[methodname]
            except KeyError:
                raise NotImplementedError(
                    "{} is missing.".format(methodident))
            metamethod = metadict[methodname]

            if prop(metamethod):
                if not prop(clsmethod):
                    raise NotImplementedError(
                        "{} must be a property.".format(methodident))
                continue  # Properties have no ArgSpec.

            metaspec = inspect.getargspec(metamethod)
            clsspec = inspect.getargspec(clsmethod)
            if not metaspec == clsspec:
                msg = ("Signatures do not match:\n"
                       "{metaname}.{methodname}()\t{metaspec}\n"
                       "{clsname}.{methodname}()\t{clsspec}"
                       "".format(**locals()))
                raise NotImplementedError(msg)
        return super(AbstractInterface, meta).__new__(meta, clsname,
                                                      bases, clsdict)

Now to define an interface we could do:

class ICar(AbstractInterface):

    def honk(self, volume=10, length=2):
        pass

    def accelerate(self, stepsize):
        pass

    @property
    def speed(self):
        pass

Applying the interface as a metaclass will enforce its methods/properties to be implemented:

class SportsCar(object):
    __metaclass__ = ICar

    @property
    def speed(self):
        pass

    def accelerate(self, stepsize):
        pass

    def honk(self, volume=10, length=2):
        pass

A missing method or differing method (in regards to variable name, default value, being a property) will raise a

NotImplementedError

during class definition. Cheers!