Reputation:
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
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