Hello! This is my first post so I’m not completely sure if I’m posting in the right category, so please let me know if that’s not the case.
I was wondering if the following would be considered a bug: when a function is wrapped with functools.cache or functools.lru_cache, the result is not functools.wraps’d with the given user function, so the following is what I get in the python console:
>>> functools.lru_cache(my_f)
<function my_f at 0x7f1c59ccbec0>
>>> functools.cache(my_f)
<function my_f at 0x7f1c59ccbec0>
My use case is within Django, specifically in the migrations serializer: currently Django knows how to serialize functions and other types, so callables can be used to populate some model fields. But if the callable is wrapped with any of the caching helpers mentioned above, the migration’s writer can not proceed with the serialization. While we could special case this “functools._lru_cache_wrapper” scenario in the Django code base, I think this is worth discussing in the Python context whether there is a bug in how the cache decorators are wrapping the given user_function.
FWIW the caches invoke update_wrapper(wrapper, user_function) to copy the metadata from the original function. A quick test shows that it is working as intended — the name, annotations, docstring, and module are all copied over:
>>> def my_f(a: int=1, b: int=2):
... 'My f doc'
... return a + b
...
>>> cached_my_f = cache(my_f)
>>> help(cached_my_f)
Help on _lru_cache_wrapper in module __main__:
my_f(a: int = 1, b: int = 2)
My f doc
From a user’s point of view, the only thing that is changed in the data type. It is an _lru_cache_wrapper object instead of an instance of function. So presumably Django’s serializer is relying on the type which is something that wraps() can’t change.
Rather than special case this private type, perhaps the serializer can use the __wrapped__ attribute to get back to the original function:
cached_my_f.__wrapped__
<function my_f at 0x103dd71a0>
Thank you @rhettinger for your answer. While I did note that update_wrapper was returned from the cache and lru_cache decorators, I was sort of expecting to be able to just use the decorated my_f as if I would have been wrapped with a decorator that uses functools.wraps, where the result of the decorator is the function itself:
>>> import functools
>>> def my_f(a: int=1, b: int=2):
... return a + b
...
>>> def my_deco(f):
... @functools.wraps(f)
... def inner(*args, **kwargs):
... # Do something interesting
... return f(*args, **kwargs)
... return inner
...
>>> my_f
<function my_f at 0x7f130849dee0>
>>> my_deco(my_f)
<function my_f at 0x7f130849e160>
I always had this assumption that a decorator that “plays nice” is such that it always uses functools.wraps when decorating a given function. Is this assumption/idea incorrect?
All that wraps does is call update_wrapper. It just changes the argument arrangement so it can be done via decorator instead. The difference is that in your case you’re returning another function object (inner), but the cache decorators can’t do that since _lru_cache_wrapper also exposes some methods.