user12128336
user12128336

Reputation:

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

Upvotes: 3

Views: 4972

Answers (1)

Tom Karzes
Tom Karzes

Reputation: 24062

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.

Upvotes: 1

Related Questions