4

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.

Everett Toews
  • 10,337
  • 10
  • 44
  • 45
  • If my understanding of what you are trying to do is correct, I think class decorators would be more suitable and less complex to accomplish this. Have you given them a shot? – Dimitris Fasarakis Hilliard Jan 07 '16 at 22:51
  • 1
    1. What are you actually trying to achieve. 2. What is the full error traceback? – jonrsharpe Jan 07 '16 at 22:51
  • is [this SO QA](http://stackoverflow.com/questions/11349183/how-to-wrap-every-method-of-a-class) relevant? – Pynchia Jan 07 '16 at 22:58
  • `library_class_dict` is a reference to the class `dictproxy` object, and it doesn't support assignment. Why are you trying to alter a baseclass from the metaclass? Why not add overrides in the new class that call their `super()` method instead? – Martijn Pieters Jan 07 '16 at 22:58
  • @MartijnPieters that is what I wanted to ask and suggest from the very beginning, but I assumed the OP wants to keep code to a minimum and/or has many methods to wrap (and I do not know much about meta's) – Pynchia Jan 07 '16 at 23:02
  • 1
    @Pynchia: well, trying to alter a base class (`LibraryClass` here) every time you subclass is.. redundant. You only need to alter that base class once, and you don't need to use a metaclass for this. – Martijn Pieters Jan 07 '16 at 23:07
  • @Pynchia is correct in that there are many methods to wrap and I want to keep the code to a minimum. – Everett Toews Jan 08 '16 at 16:36
  • @MartijnPieters I'm totally open to alternative implementations. – Everett Toews Jan 08 '16 at 16:38

2 Answers2

4

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.

Everett Toews
  • 10,337
  • 10
  • 44
  • 45
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

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.

Mike Müller
  • 82,630
  • 20
  • 166
  • 161