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
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
“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.
Yeah, the big distinction here, in my mind, is that metaclasses are inherited and class decorators are not.