2

I want to create metaclass which will decorate every function with trace decorator.

So I got this:

from functools import wraps
from inspect import getfile

from arrow import now


def trace(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        print(
            '{timestamp} - {file} - {function} - CALL *{args} **    {kwargs}'.format(timestamp=now().isoformat(sep=' '),
                                                                                                 file=getfile(f),
                                                                                 function=f.__name__, args=args[1:],
                                                                                 kwargs=kwargs))
        result = f(*args, **kwargs)
        print(
            '{timestamp} - {file} - {function} - RESULT     {result}'.format(timestamp=now().isoformat(sep=' '),
                                                                         file=getfile(f),
                                                                         function=f.__name__,
                                                                         result=result))
        return result

    return wrapper


class TraceLogger(type):
    def __new__(mcs, name, bases, dct):
        for attr in dct:
            value = dct[attr]
            if callable(value):
                dct[attr] = trace(value)
        return super(TraceLogger, mcs).__new__(mcs, name, bases, dct)


class ExampleClass(object):
    __metaclass__ = TraceLogger

    def foo(self):
        print('foo')

    @staticmethod
    def bar():
        print('bar')

example = ExampleClass()
example.foo()
example.bar()

Tracing works for any non static functions because staticmethods are not callable. How can I unwrap staticmethod then wrap it two times in new metclass like this:

dct[attr] = staticmethod(trace(value))
  • What do you mean by `staticmethods are not callable`? They are, otherwise you wouldn't be able to call them – DeepSpace Dec 09 '18 at 16:03
  • Hi @DeepSpace staticmethod objects don't meet this condition if callable(value): dct[attr] = trace(value) – don_cappucino Dec 09 '18 at 16:16
  • 1
    I understand. Your question boils down to "Why `callable(ExampleClass.bar)` returns `True` while `callable(ExampleClass.__dict__['bar'])` returns `False`" which is a very interesting question. Perhaps you want to simplify your question a bit as this is not related to the metaclass. – DeepSpace Dec 09 '18 at 16:18
  • `staticmethod` objects are descriptors, and when accessing them, Python returns a callable function. They themselves are indeed not callable. – L3viathan Dec 15 '18 at 17:01

2 Answers2

2

You can unwrap a staticmethod object by calling __get__ on it.

@staticmethod
def func(*args):
    print('func called:', args)
    return 42

print(func)
print(func.__get__(None, object))
print(func.__get__(None, object)(1, 2, 3))

It outputs:

<staticmethod object at 0x7f8d42835ac0>
<function func at 0x7f8d429561f0>
func called: (1, 2, 3)
42

As for why this works, you may be interested in learning about what the descriptor protocol is, and I recommend this link.

GZZ
  • 41
  • 7
1

(I linked to three different questions/answers in this answer because I wanted to provide as much details as possible rather than just close as duplicate. If you upvote this answer please consider to upvote the linked answers as well)

You have stumbled upon an interesting 'feature' of Python which is explained in the answers of this question.

Instead of if callable(value) you could have checked if isinstance(value, (function, staticmethod, classmethod)) but this would only lead to another interesting corner-case: NameError: name 'function' is not defined (see why here) (Even doing import builtins ; ... ; builtins.function will lead to an error).

You can't get away from the need to check if an attribute name is a method, staticmethod or a classmethod, and the (arguably, see here why) correct way in your case to do it is to use types.FunctionType:

import types

...

if type(value) == types.FunctionType: # or isinstance(value, types.FunctionType)
    dct[attr] = trace(value)

...
DeepSpace
  • 78,697
  • 11
  • 109
  • 154