Virtual Static Checking With Python Metaclasses

Virtual Static Checking With Python Metaclasses

What are metaclasses

Objects are first-class citizens in python: every class is an object.

Every time we declare a class, under the hood python is actually instantiating a class that defines classes. Enter the meta-class.

Python’s metaclass is type, which, being itself a class, can be inherited and extended.

The signature of metaclass constructors is:

type(class_name, base_classes, attributes)

and with this call you can create a class definition anywhere.

Which means this:

class MyClass:
    atttribute1 = 'foobar'

could also be expressed like this:

MyClass = type('MyClass', (object, ), {'attribute1': 'foobar'})

Custom metaclasses

To define a metaclass we must inherit from a metaclass, so let’s start with type.

The __new__ method will be invoked during class declaration.

class MyMeta(type):
    def __new__(cls, name, bases, attributes):
        attributes['__metaclasses_are_awesome__'] = True

        cls = type.__new__(cls, name, bases, attributes)
        return cls

class MyClass(object, metaclass=MyMeta):
    pass

assert hasattr(MyClass, '__metaclasses_are_awesome__')
assert MyClass.__metaclasses_are_awesome__ == True

Implementing Virtual Static-checking

We can use meta-classes as hooks that check if the subclass declarations of such meta-class is properly done.

To illustrate this let’s design a DSL to declare RESTful endpoints with classes.

Our DSL should consider the following declaration valid:

class Settings(Resource):
    route = '/api/settings'

    def get(self):
        return {
            'foo': 'bar'
        }

We want to enforce the declaration of a route class-attribute being a string, so this declarations should be invalid:

class Settings(Resource):
    route = {'not a string'}

    def get(self):
        return {
            'foo': 'bar'
        }

class ResourceMeta(type):
    def __new__(cls, name, bases, attributes):
        cls = type.__new__(cls, name, bases, attributes)

        if name in ('ResourceMeta', 'Resource'):
            # prevent processing the metaclass and resource baseclass.
            return cls

        route = attributes.get('route')
        if not route:
            raise TypeError(f'{cls} must declare a "route" attribute')

        if not isinstance(route, str):
            raise TypeError(
                f'{cls}.route must be a string, '
                f'got {type(route)} instead')
            )

        return cls


class Resource(object, metaclass=ResourceMeta):
    pass

Now, trying to declare a resource without an invalid route:

class User(Resource):
    route = {'not a string'}

    def get(self):
        return {
            'email': 'john@doe.com'
        }

Would cause a TypeError during import time.

Show Comments