The @wraps(func) can only keep the function has the same text information with the original function, while the information of the __code__, __closure__ cannot be visited easily.
Here is my idea:
We can use the code below to proxy the decorator:
class WrapProxy:
def __init__(self, decorator, extern_object=None):
if isinstance(extern_object, ObjectProxy):
self.__func_list__ = extern_object.__func_list__.copy()
else:
self.__func_list__ = []
self.decorator = decorator
def __call__(self, func):
if isinstance(func, ObjectProxy):
return ObjectProxy(decorator, func.__wrapped__, func.__func_list__ + self.__func_list__)
return ObjectProxy(decorator, func, [func] + self.__func_list__)
class ObjectProxy:
def __init__(self, decorator, func, func_list):
self.__wrapped__ = decorator(func)
self.__func_list__ = func_list
# Proxy the other method for "self.__wrapped__", such as "__getattr__", "__getitem__", "__call__", "__get__" and so on
Then we can use the syntax like this to get the original wrapped function:
In this case, we can visit my_func.__wrapped__ to get the result, and my_func.__func_list__ to get the original func list (in this case it will contain two different functions “my_func”). Some code can use the __func_list__ to get the information for the original func information(for example: __code__, __closure__).
@wraps already provides access to the wrapped function via the __wrapped__ attribute, with which you can use a while loop to access a chain of wrapped functions, so it isn’t clear to me what value your proposed wrapper class is providing here.
And In this case, you cannot visit two or more functions by one way (the other class decoration may won’t use f_get, f_set, f_delete attribute to save the original func).
It is that functools.wraps won’t be used in some classes (e.g. if you use the way just to analyse __wrapped__ to get the original function, here will fail). And if you use @wraps for your class instance to decorated another function, the next function will cover the original __wrapped__. Here I supply a way that can exactly record all of the original functions be decorated.
One project may use many decorators from many modules, but some authors forgot to use @wraps(func) or its decorator cannot use it.
For class instance, if you use a method to decorate another function with @wraps(func), it will cover the other __wrapped__ and miss the original function information.
This tool can help people safely decorating their functions and tracing all primitive functions, even though some decorators don’t keep the information.
This tool can always record the original function. When you put another result as the second param, the function list will merge. Finally, the function list will be [my_func1, my_func2...] while the result can be kept in __wrapped__, even though one author made a mistake or he was unable to use it which leads to that the function chain __wrapped__ fractured.
There’s no way that creating WrapProxy will remind people to use wraps.
I get why this does what you want, but it’s not clear that these are common problems. You have the tools to create wrappers that can be inspected differently, so I don’t see a problem at all. There’s nothing more the language needs to do to support your use case.
In general, I would treat inspecting object internals as a code smell. Your code will become more complicated and fragile as you ignore the interface the object presents to find some “real” object underneath. Duck typing allows you to write code that operates on an interface and have it continue working when some new object that presents the same interface is given to you. If you spend your time inspecting, you’ll likely negate the advantages of duck typing, even if your code doesn’t break.
@property doesn’t follow the __wrapped__ convention for good reasons because @property wraps up to three functions, not one. That is, is MyClass.a.__wrapped__ supposed to point to fget, fset or fdel of MyClass.a?
Best to special-case such wrappers in your inspection tools. There shouldn’t be that many that need special-casing. If a wrapper type that wraps only one function does not expose its wrapped function as __wrapped__, it should be made to do so. If you don’t own the wrapper’s code, ask the owner to make it follow the convention.
Note that inspect.unwrap and inspect.signature(callable, follow_wrapped=True) do expect wrapper functions to follow the __wrapped__ convention so codes that don’t follow the convention should be “fixed” unless for exceptional reasons.
For example, I defined a class, it has a method, and it can wrap many functions (just like property), and then which function should __wrapped__ point?
The attributes for the type wrapper to store are different and have no standard.
Exactly my point of special-casing those special types according to what you want to do to those multiple wrapped functions with your specific inspection tool. Maybe you want to put them in a list. Maybe you want to recurse into each wrapped function for further unwrapping. Maybe you want to deal with each of them differently because fget, fset and fdel do distinctly different things. There’s no generic answer here and therefore there shouldn’t be a generic stdlib function that makes the decision for you.
So, if a module has the requirement for all of the original functions, should it include all kinds of the other third party modules’ type wrapers? Maybe it is really a bad idea.
If you need all of the original wrapped functions, the code in your proposal won’t help either since it only helps keep track of functions with multiple wrappers rather than wrapper types that wraps multiple functions.
Instead, you can use a recursive function with a predefined mapping of supported wrapper types mapped to their exposed attributes of wrapped functions.
from inspect import unwrap
def unwrap_all(func, follow_attrs={property: ['fget', 'fset', 'fdel']}):
result = []
unwrapped = unwrap(func)
for special_type, attrs in follow_attrs.items():
if isinstance(unwrapped, special_type):
for name in attrs:
if attr := getattr(unwrapped, name, None):
result.extend(unwrap_all(attr))
break
else:
result.append(unwrapped)
return result
On second thought, a more plausible proposal to push forward is to standardize the names of wrapper attributes as an optional non-string iterable class attribute __wrapped__ so that it may be possible to have an inspect.unwrap_all function in the stdlib to unwrap property-like descriptor instances from third-party modules in a standardized way.
So we may have:
from inspect import unwrap_all
class Property(property):
__wrapped__ = 'fget', 'fset', 'fdel'
class MyClass:
@Property
def a(self):
return self._a
@a.setter
def a(self, value):
self._a = 1
print(unwrap_all(MyClass.a)) # [<function MyClass.a at 0x0000027628412FB0>, <function MyClass.a at 0x0000027628413110>]