Everett Toews
Everett Toews

Reputation: 10956

Add a MetaClass to a library class

In a Python library I'm using, I want to wrap the public methods of a class from the library. I'm trying to use a MetaClass to do this like so.

from functools import wraps
from types import FunctionType
from six import with_metaclass

def wrapper(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        print("wrapped %s" % func)
        return func(*args, **kwargs)
    return wrapped

class MetaClass(type):
    def __new__(mcs, classname, bases, class_dict):
        library_class_dict = bases[0].__dict__

        print(library_class_dict)

        for attributeName, attribute in library_class_dict.items():
            if type(attribute) == FunctionType and \
                    not attributeName.startswith('_'):
                print("%s %s" % (attributeName, attribute))
                attribute = wrapper(attribute)

            library_class_dict[attributeName] = attribute

        print(library_class_dict)

        return type.__new__(mcs, classname, bases, class_dict)

# this is the class from the library that I cannot edit
class LibraryClass(object):
    def library_method(self):
        print("library method")

class Session(with_metaclass(MetaClass, LibraryClass)):

    def __init__(self, profile, **kwargs):
        super(Session, self).__init__(**kwargs)
        self.profile = profile

When you put this into a Python file and run it, you get the error

TypeError: Error when calling the metaclass bases
    'dictproxy' object does not support item assignment

I get that trying to assign directly to __dict__ is a bad idea. That's not what I want to do. I would much rather add the MetaClass to the LibraryClass but I'm not sure how.

I've been through the other StackOverflow questions regarding Python MetaClass programming but haven't come across any that try to add a MetaClass to a library class where you can't the source code.

Upvotes: 4

Views: 255

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1121834

You can't assign to a dictproxy. Use setattr() to set attributes on a class:

setattr(bases[0], attributeName, attribute)

However, you don't need a metaclass to do this, which is entirely overkill here. You can just do this on that base class, and do it once:

for attributeName, attribute in vars(LibraryClass).items():
    if isinstance(attribute, FunctionType) and not attributeName.startswith('_'):
        setattr(LibraryClass, attributeName, wrapper(attribute))

This just does it once, rather than every time you create a subclass of of LibraryClass.

Upvotes: 4

Mike Müller
Mike Müller

Reputation: 85442

Essentially you want to do this:

LibraryClass.library_method = wrapper(LibraryClass.library_method)

for all methods automatically.

Using your code pieces:

from functools import wraps
from types import FunctionType

class LibraryClass(object):
    def library_method(self):
        print("library method")

def wrapper(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        print("wrapped %s" % func)
        return func(*args, **kwargs)
    return wrapped

You can write a helper function that does that for all methods:

def wrap_all_methods(cls):
    for name, obj in cls.__dict__.items():
        if isinstance(obj, FunctionType) and not name.startswith('_'):
            setattr(cls, name, wrapper(obj))
    return cls

Now, wrap all methods:

LibraryClass = wrap_all_methods(LibraryClass)

Test if it works:

class Session(LibraryClass):
    pass

s = Session()

s.library_method()

prints:

wrapped <function LibraryClass.library_method at 0x109d67ea0>
library method

The method is wrapped.

Upvotes: 0

Related Questions