3
votes

I made a decorator that would modify a class so that any method other than init or class would print "Hello World" before running. The issue is I keep getting an unexpected error message when I inherit from dict. What does it mean and how do I solve it?

Code

from functools import wraps


# Print hello world when any method is called
def hello_world(cls):
    for attr in dir(cls):
        # Only modify methods that aren't init or class
        if not callable(getattr(cls, attr)) or any([method in attr for method in ["init", "class"]]):
            continue
        old_method = getattr(cls, attr)

        # Modify method to print "Hello World" before running
        @wraps(getattr(cls, attr))
        def new_method(self, *args, **kwargs):
            print("Hello World!")
            return old_method(self, *args, **kwargs)

        # Update class with modified method
        setattr(cls, attr, new_method)
    return cls


@hello_world
class Custom(dict):
    pass


dictionary = Custom()
dictionary["Key"] = "Value"
print(dictionary)

Output

Hello World!
Traceback (most recent call last):
    File "", line 22, in
        dictionary = Custom()
    File "", line 12, in new_method
        return old_method(self, *args, **kwargs)
TypeError: descriptor 'values' for 'dict' objects doesn't apply to a 'type' object

1
I suspect you meant to say or method in ('__init__','__class__') in your if statement. That doesn't fix the error, however. - Tim Roberts
One big problem is that you are not "capturing" the value of old_method in your inner function. By the time you're done, all of the new_method instances are referring to the old_value in the function, which finishes with values (alphabetically). That is, ALL of the functions will now call values. - Tim Roberts
@TimRoberts And here I am sitting and staring at this looking for deeper meaning. Thanks for saving me the trouble of feeling like an idiot later. I'll let you do the honors of posting an answer if you like :P - Silvio Mayolo
I considered that, but it isn't really an "answer" because I can't tell you how to fix it. At this point, I'm not entirely convinced it CAN be fixed. - Tim Roberts
@TimRoberts Nice observation about the old_method value not being captured. Based on that insight, I was able to create a workaround for the problem. See my posted answer. - Tom Karzes

1 Answers

1
votes

Thanks to the insight by Tim Roberts, I was able to cobble together a workaround for the problem. I had to add a couple more attributes to the exclude list to avoid infinite recursion, and I added an additional debugging statement so that you could see which method was being invoked when printing "Hello world!".

Anyway, the following works:

from functools import wraps


# Print hello world when any method is called
def hello_world(cls):
    for attr in dir(cls):
        # Only modify methods that aren't init or class
        if not callable(getattr(cls, attr)):
            continue

        if attr in ("__class__",
                    "__subclasshook__",
                    "__init__",
                    "__init_subclass__",
                    "__str__",
                    "__repr__"):
            continue

        old_method = getattr(cls, attr)

        def do_override(cls, attr, old_method):
            # Modify method to print "Hello World" before running
            @wraps(getattr(cls, attr))
            def new_method(self, *args, **kwargs):
                print("Hello World!")
                print("attr:", attr)
                return old_method(self, *args, **kwargs)

            # Update class with modified method
            setattr(cls, attr, new_method)

        do_override(cls, attr, old_method)

    return cls


@hello_world
class Custom(dict):
    pass


dictionary = Custom()
dictionary["Key"] = "Value"
print(dictionary)

When run, it produces:

Hello World!
attr: __new__
Hello World!
attr: __setitem__
{'Key': 'Value'}

The do_override function captures the values of cls, attr, and old_method so that subsequent loop iterations don't affect the previously bound values.

In the output, the first method invocation is from when the class instance is created, and the second is from when a value is set.

This will probably need some work to be what you want, and you may need to add some additional attributes to the exclude list, but this should get you past the problems you encountered.