3

I want to implement a metaclass for wrapping methods to log additional information. But I also need to have abstractmethods. I tried to extend ABCMeta but it doesn't seem to enforce the @abstractmethod decorator:

import types
import abc

def logfunc(fn, *args, **kwargs):
    def fncomposite(*args, **kwargs):
        rt = fn(*args, **kwargs)
        print("Executed %s" % fn.__name__)
        return rt
    return fncomposite

class LoggerMeta(abc.ABCMeta):
    def __new__(cls, clsname, bases, dct):
        for name, value in dct.items():
            if type(value) is types.FunctionType or type(value) is types.MethodType:
                dct[name] = logfunc(value)
        return super(LoggerMeta, cls).__new__(cls, clsname, bases, dct)

    def __init__(cls, *args, **kwargs):
        super(LoggerMeta, cls).__init__(*args, **kwargs)
        if cls.__abstractmethods__:
            raise TypeError("{} has not implemented abstract methods {}".format(
                cls.__name__, ", ".join(cls.__abstractmethods__)))


class Foo(metaclass=LoggerMeta):
    @abc.abstractmethod
    def foo(self):
        pass

class FooImpl(Foo):
    def a(self):
        pass

v = FooImpl()
v.foo() 

When I run this it prints Executed foo. However I expected it to fail because I have not implemented foo in FooImpl.

How can I fix this?

MSeifert
  • 145,886
  • 38
  • 333
  • 352
budchan chao
  • 327
  • 3
  • 15
  • Well ... you decorated (replaced) your abstractmethod with a non abstract function in your metaclass `__new__`. What did you expect? – MSeifert Jul 05 '18 at 15:32
  • You could instead only decorate non abstract members. That should work. – MSeifert Jul 05 '18 at 15:35
  • Well I also presumed that to be the case. But I want abstract members logged as well. Guess I am not familiar with how ABCMeta is implemented. I thought as long a we don't change the function name it shouldn't matter which actual function it points to when it comes to the abstract method check. – budchan chao Jul 05 '18 at 15:46

1 Answers1

2

The problem is that when you decorate a function (or method) and return a different object you effectively replaced the function (method) with something else. In your case the method isn't an abstractmethod any more. It's a function that wraps an abstractmethod, which isn't recognized as abstract by ABCMeta.

The fix is relatively easy in this case: functools.wraps:

import functools  # <--- This is new

def logfunc(fn, *args, **kwargs):
    @functools.wraps(fn)   # <--- This is new
    def fncomposite(*args, **kwargs):
        rt = fn(*args, **kwargs)
        print("Executed %s" % fn.__name__)
        return rt
    return fncomposite

That's all you need to change.

And with that change in place it correctly raises:

TypeError: Foo has not implemented abstract methods foo

However you don't need LoggerMeta.__init__ anymore. You can simply let ABCMeta handle the case when there are un-implemented abstract methods. Without the LoggerMeta.__init__ method this will raise another exception:

TypeError: Can't instantiate abstract class FooImpl with abstract methods foo

The functools.wraps not only correctly handles abstractmethods. It also preserves the signature and documentation of the decorated function (and some other nice stuff). If you use decorators to simply wrap functions you almost always want to use functools.wraps!

MSeifert
  • 145,886
  • 38
  • 333
  • 352