Search code examples
pythondictionaryoopdecorator

TypeError: descriptor 'values' for 'dict' objects doesn't apply to a 'type' object


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


Solution

  • 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.