You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
singledispatch.register fails for lru_cache decorated functions #132064
Also, i'd say there are at least two things wrong here:
The error message is very cryptic (i'm still not 100% sure what exactly is wrong)
Either lru_cache is not wrapping the function properly or .register is not picking up the type hints correctly (maybe a fallback to the old behaviour?)
for reference, it worked as far back as i've tested, aka down to 3.9. i do realise it might not have been intended to work though, and so this is not necessarily a feature.
functools.lru_cache copies essential attributes from the wrapped function with functools.update_wrapper. Its current default tuple of attributes is WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__'). That tuple was last updated by @JelleZijlstra in 2024. Perhaps it should include __annotations__ as well. Also contact @ambv to verify that he expects this to work.
It is fixable in _functoolsmodule.c by adding an entry to lru_cache_getsetlist for __annotations__, providing both a getter and a setter that mirror those for the __annotations__ attribute on functions. The setter should unconditionally set __annotations__ in the __dict__; the getter should return __annotations__ from the __dict__ if available, else try to call __annotate__ if it's in the dict, and cache the result. Anyone interested in implementing that?
@JelleZijlstra, The lru_cache has a pure python implementation as well. The wrapping of both the pure python version and the C version is done in functools.py with update_wrapper. So, I don't think a C edit is a complete or correct solution.
Can this be fixed for the general case by expanding WRAPPER_ASSIGNMENTS to include __annotations__? The guidance says, "The annotate and annotations attributes should both be supplied with semantics consistent to those of the wrapped object." So it looks to me like __annotations__ should be added back to WRAPPER_ASSIGNMENTS:
Looking at the lru_cache wrapping code in pure python, I don't see any part of the current issue that is specific to the lru_cache. AFAICT, this would happen to any wrapped function.
No, adding __annotations__ to WRAPPER_ASSIGNMENTS would be incorrect because it would immediately force evaluation of the annotations of any wrapped function.
The Python implementation of lru_cache already works on 3.14, so it doesn't need any changes to fix this issue. (To test, take the OP's script, comment out the lines in functools.py that import the C implementation, and run the script.)
Another possibility is for singledispatch.register to follow the suggestion in the docs for update_wrapper, "To allow access to the original function for introspection and other purposes (e.g. bypassing a caching decorator such as lru_cache(), this function automatically adds a wrapped attribute to the wrapper that refers to the function being wrapped."
Presumably, lru_cache isn't the only decorator that was broken by the edit to WRAPPER_ASSIGNMENTS.
I found a better fix for the original issue reported here: #132195. This makes annotationlib use the __annotate__ function if present, even if __annotations__ is not also present. This should help not only @lru_cache, but any other decorator that uses update_wrapper on a wrapper that is not a Python function.
With that fix, it's still the case though that lru_cache-decorated functions don't have a direct __annotations__ attribute when the C version is used:
>>> @functools.lru_cache
... def f(x: int): pass
...
>>> f.__annotations__
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
f.__annotations__
AttributeError: 'functools._lru_cache_wrapper' object has no attribute '__annotations__'. Did you mean: '__annotate__'?
>>> f.__wrapped__.__annotations__
{'x': <class 'int'>}
>>> f.__annotate__(1)
{'x': <class 'int'>}
This could impact users who (against recommendations) access __annotations__ directly instead of using annotationlib.get_annotations or (in previous Python versions) inspect.get_annotations. To fix it, we could add a dedicated __annotations__ descriptor to the C version of functools._lru_cache_wrapper. Is that worth it? I'm not sure.
Activity
lczyk commentedon Apr 4, 2025
See here (PR to my own package) for implemented workaround, tested with tox for py39~py314 on ubuntu, macOS and windows (-latest).
lczyk commentedon Apr 4, 2025
Also, i'd say there are at least two things wrong here:
lru_cacheis not wrapping the function properly or.registeris not picking up the type hints correctly (maybe a fallback to the old behaviour?)picnixz commentedon Apr 4, 2025
@rhettinger Was
singledispatchdesigned to work on LRU-decorated functions?lczyk commentedon Apr 4, 2025
for reference, it worked as far back as i've tested, aka down to 3.9. i do realise it might not have been intended to work though, and so this is not necessarily a feature.
picnixz commentedon Apr 4, 2025
Oh so you mean that it's a 3.14+ issue?
lczyk commentedon Apr 4, 2025
yes! sorry, reading back i was not clear about that 😅 (added an
EDITto the original report to make it clearer.)picnixz commentedon Apr 4, 2025
As it uses
get_type_hints(func, format=Format.FORWARDREF), I'm asking first @JelleZijlstra. I can have a look tomorrow thoughrhettinger commentedon Apr 4, 2025
functools.lru_cachecopies essential attributes from the wrapped function withfunctools.update_wrapper. Its current default tuple of attributes isWRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__'). That tuple was last updated by @JelleZijlstra in 2024. Perhaps it should include__annotations__as well. Also contact @ambv to verify that he expects this to work.JelleZijlstra commentedon Apr 5, 2025
This is indeed a consequence of the implementation of PEP 649/PEP 749, specifically this section: https://peps.python.org/pep-0749/#wrappers-that-provide-annotations .
It is fixable in
_functoolsmodule.cby adding an entry tolru_cache_getsetlistfor__annotations__, providing both a getter and a setter that mirror those for the__annotations__attribute on functions. The setter should unconditionally set__annotations__in the__dict__; the getter should return__annotations__from the__dict__if available, else try to call__annotate__if it's in the dict, and cache the result. Anyone interested in implementing that?rhettinger commentedon Apr 6, 2025
@JelleZijlstra, The lru_cache has a pure python implementation as well. The wrapping of both the pure python version and the C version is done in
functools.pywithupdate_wrapper. So, I don't think a C edit is a complete or correct solution.Can this be fixed for the general case by expanding
WRAPPER_ASSIGNMENTSto include__annotations__? The guidance says, "The annotate and annotations attributes should both be supplied with semantics consistent to those of the wrapped object." So it looks to me like__annotations__should be added back toWRAPPER_ASSIGNMENTS:Looking at the
lru_cachewrapping code in pure python, I don't see any part of the current issue that is specific to thelru_cache. AFAICT, this would happen to any wrapped function.JelleZijlstra commentedon Apr 6, 2025
No, adding
__annotations__to WRAPPER_ASSIGNMENTS would be incorrect because it would immediately force evaluation of the annotations of any wrapped function.JelleZijlstra commentedon Apr 6, 2025
The Python implementation of lru_cache already works on 3.14, so it doesn't need any changes to fix this issue. (To test, take the OP's script, comment out the lines in functools.py that import the C implementation, and run the script.)
rhettinger commentedon Apr 6, 2025
Another possibility is for
singledispatch.registerto follow the suggestion in the docs forupdate_wrapper, "To allow access to the original function for introspection and other purposes (e.g. bypassing a caching decorator such as lru_cache(), this function automatically adds a wrapped attribute to the wrapper that refers to the function being wrapped."Presumably,
lru_cacheisn't the only decorator that was broken by the edit toWRAPPER_ASSIGNMENTS.JelleZijlstra commentedon Apr 7, 2025
I found a better fix for the original issue reported here: #132195. This makes annotationlib use the
__annotate__function if present, even if__annotations__is not also present. This should help not only@lru_cache, but any other decorator that usesupdate_wrapperon a wrapper that is not a Python function.With that fix, it's still the case though that
lru_cache-decorated functions don't have a direct__annotations__attribute when the C version is used:This could impact users who (against recommendations) access
__annotations__directly instead of usingannotationlib.get_annotationsor (in previous Python versions)inspect.get_annotations. To fix it, we could add a dedicated__annotations__descriptor to the C version of functools._lru_cache_wrapper. Is that worth it? I'm not sure.gh-132064: Make annotationlib use __annotate__ if only it is present (#…