Welcome, guest | Sign In | My Account | Store | Cart

Abstract Bases Classes in Python provide great features for describing interfaces programmatically. By default a subclass is validated against all its ABC parents at instantiation time (in object.__new__). This recipe aims to provide for validation against an ABC of:

  • any class at definition time (including subclasses and registered classes),
  • any object at any time.

I have included an example of the reason I did all this. It allows you to implement an ABC in the instance rather than the class.

If the classes argument to validate is None then it tries to build the list of classes from the object's MRO. If the ABCMeta.register method facilitated an __implements__ list on classes, we could also use that to validate against the registered "base" classes.

The code I have provided is for Python 3, but it should work in 2.7 with a little modification.

This code borrows from Lib/abc.py and objects/typeobject.c

Python, 45 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def validate(obj, classes=None):
    """
    Validate the object against the classes.

    This is like isinstance, except that it validates that the object 
    has implemented all the abstract methods/properties of all the 
    classes.

    """
    if classes is None:
        classes = obj.mro()
        #classes.extend(obj.__implements__)
    if not isinstance(classes, (list, tuple)):
        classes = (classes,)
    abstracts = set()
    for cls in classes:
        if not isinstance(cls, type):
            raise TypeError("Can only validate against classes")
        for name in getattr(cls, "__abstractmethods__", set()):
            value = getattr(obj, name, None)
            if not value:
                abstracts.add(name)
            elif getattr(value, "__isabstractmethod__", False):
                abstracts.add(name)
    if abstracts:
        sorted_methods = sorted(abstracts)
        joined = ", ".join(sorted_methods)
        try:
            name = obj.__name__
        except AttributeError:
            name = obj
        msg = "{} does not implement abstract methods {}"
        raise TypeError(msg.format(name, joined))


def conforms(classes):
    """A class decorator factory for validating against an ABC."""
    def decorator(cls):
        if not __debug__:
            return cls
        if not isinstance(cls, type):
            raise TypeError("Can only validate classes")
        validate(cls, classes)
        return cls
    return decorator

The motivating example:

from abc import abstractmethod, abstractproperty, ABCMeta

class X(metaclass=ABCMeta):
    @abstractproperty
    def name(self): pass
    @abstractmethod
    def f(self): pass

@X.register(C)
class C:
    def __init__(self, name):
        self.name = name
    def f(self): pass

obj = C("some name")
validate(obj, X)

# this works

Here are a bunch more examples:

A (not subclass) does not implement name or f:

@conforms(X)
class A:
    pass

# raises TypeError

A (not subclass) implements name and f:

@conforms(X)
class A:
    name = "some name" # just has to be bound...
    def f(self): pass

# this works

B (subclass) does not implement name or f:

@conforms(X)
class B(X):
    pass

# raises TypeError

B (subclass) implements name and f:

@conforms(X)
class B(X):
    name = "some name" # just has to be bound...
    def f(self): pass

# this works

C (not sublcass) implements f, but name is implemented only on instance:

@conforms(X)
class C:
    def __init__(self, name):
        self.name = name
    def f(self): pass

# raises TypeError

C (not sublcass) implements f, but name is implemented only on instance:

class C:
    def __init__(self, name):
        self.name = name
    def f(self): pass

obj = C("some name")
validate(obj, X)

# this works

C (not sublcass) implements neither f nor name; both are implemented only on instance:

class C:
    def __init__(self, name):
        self.name = name
        self.f = lambda: 1

obj = C("some name")
validate(obj, X)

# this works

2 comments

Éric Araujo 12 years, 11 months ago  # | flag

“Doing X at class creation time” rings the metaclass bell in my head. A class decorator works too, but for some reason I like using a metaclass.

Eric Snow (author) 12 years, 11 months ago  # | flag

Yeah, the big distinction here, in my mind, is that metaclasses are inherited and class decorators are not.

Created by Eric Snow on Fri, 20 May 2011 (MIT)
Python recipes (4591)
Eric Snow's recipes (39)

Required Modules

Other Information and Tasks