You can't do what you want with just ABCMeta
. ABC enforcement doesn't do any type checking, only the presence of an attribute with the correct name is enforced.
Take for example:
>>> from abc import ABCMeta, abstractmethod, abstractproperty
>>> class Abstract(object):
... __metaclass__ = ABCMeta
... @abstractmethod
... def foo(self): pass
... @abstractproperty
... def bar(self): pass
...
>>> class Concrete(Abstract):
... foo = 'bar'
... bar = 'baz'
...
>>> Concrete()
<__main__.Concrete object at 0x104b4df90>
I was able to construct Concrete()
even though both foo
and bar
are simple attributes.
The ABCMeta
metaclass only tracks how many objects are left with the __isabstractmethod__
attribute being true; when creating a class from the metaclass (ABCMeta.__new__
is called) the cls.__abstractmethods__
attribute is then set to a frozenset
object with all the names that are still abstract.
type.__new__
then tests for that frozenset
and throws a TypeError
if you try to create an instance.
You'd have to produce your own __new__
method here; subclass ABCMeta
and add type checking in a new __new__
method. That method should look for __abstractmethods__
sets on the base classes, find the corresponding objects with the __isabstractmethod__
attribute in the MRO, then does typechecking on the current class attributes.
This'd mean that you'd throw the exception when defining the class, not an instance, however. For that to work you'd add a __call__
method to your ABCMeta
subclass and have that throw the exception based on information gathered by your own __new__
method about what types were wrong; a similar two-stage process as what ABCMeta
and type.__new__
do at the moment. Alternatively, update the __abstractmethods__
set on the class to add any names that were implemented but with the wrong type and leave it to type.__new__
to throw the exception.
The following implementation takes that last tack; add names back to __abstractmethods__
if the implemented type doesn't match (using a mapping):
from types import FunctionType
class ABCMetaTypeCheck(ABCMeta):
_typemap = { # map abstract type to expected implementation type
abstractproperty: property,
abstractstatic: staticmethod,
# abstractmethods return function objects
FunctionType: FunctionType,
}
def __new__(mcls, name, bases, namespace):
cls = super(ABCMetaTypeCheck, mcls).__new__(mcls, name, bases, namespace)
wrong_type = set()
seen = set()
abstractmethods = cls.__abstractmethods__
for base in bases:
for name in getattr(base, "__abstractmethods__", set()):
if name in seen or name in abstractmethods:
continue # still abstract or later overridden
value = base.__dict__.get(name) # bypass descriptors
if getattr(value, "__isabstractmethod__", False):
seen.add(name)
expected = mcls._typemap[type(value)]
if not isinstance(namespace[name], expected):
wrong_type.add(name)
if wrong_type:
cls.__abstractmethods__ = abstractmethods | frozenset(wrong_type)
return cls
With this metaclass you get your expected output:
>>> class Abstract(object):
... __metaclass__ = ABCMetaTypeCheck
... @abstractmethod
... def foo(self): pass
... @abstractproperty
... def bar(self): pass
... @abstractstatic
... def baz(): pass
...
>>> class ConcreteWrong(Abstract):
... foo = 'bar'
... bar = 'baz'
... baz = 'spam'
...
>>> ConcreteWrong()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class ConcreteWrong with abstract methods bar, baz, foo
>>>
>>> class ConcreteCorrect(Abstract):
... def foo(self): return 'bar'
... @property
... def bar(self): return 'baz'
... @staticmethod
... def baz(): return 'spam'
...
>>> ConcreteCorrect()
<__main__.ConcreteCorrect object at 0x104ce1d10>