diff --git a/Lib/functools.py b/Lib/functools.py index b1f1fe8d9a6f27..ccc9fad1f56fa8 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -476,6 +476,8 @@ def _make_key(args, kwds, typed, return key[0] return _HashedSeq(key) +_LRU_CACHE_WRAPPER_ASSIGNMENTS = frozenset(WRAPPER_ASSIGNMENTS).union( + ('__defaults__', '__kwdefaults__')) def lru_cache(maxsize=128, typed=False): """Least-recently-used cache decorator. @@ -510,7 +512,7 @@ def lru_cache(maxsize=128, typed=False): user_function, maxsize = maxsize, 128 wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed} - return update_wrapper(wrapper, user_function) + return update_wrapper(wrapper, user_function, assigned=_LRU_CACHE_WRAPPER_ASSIGNMENTS) elif maxsize is not None: raise TypeError( 'Expected first argument to be an integer, a callable, or None') @@ -518,7 +520,7 @@ def lru_cache(maxsize=128, typed=False): def decorating_function(user_function): wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed} - return update_wrapper(wrapper, user_function) + return update_wrapper(wrapper, user_function, assigned=_LRU_CACHE_WRAPPER_ASSIGNMENTS) return decorating_function diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 3320ab7ec6649d..a6104547325c4d 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1728,6 +1728,18 @@ def test_staticmethod(x): for ref in refs: self.assertIsNone(ref()) + def test_lru_defaults_bug44003(self): + @self.module.lru_cache(maxsize=None) + def func(arg='ARG', *, kw: str = 'KW'): + return arg, kw + + self.assertEqual(func.__wrapped__.__annotations__, {'kw': str}) + self.assertEqual(func.__wrapped__.__defaults__, ('ARG',)) + self.assertEqual(func.__wrapped__.__kwdefaults__, {'kw': 'KW'}) + self.assertEqual(func.__annotations__, {'kw': str}) + self.assertEqual(func.__defaults__, ('ARG',)) + self.assertEqual(func.__kwdefaults__, {'kw': 'KW'}) + @py_functools.lru_cache() def py_cached_func(x, y): diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 19cfa9b07b340d..0aa2cf0be28a5c 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -19,6 +19,7 @@ typedef struct _functools_state { PyTypeObject *partial_type; PyTypeObject *keyobject_type; PyTypeObject *lru_list_elem_type; + PyObject *lru_obj_attrs_to_clone; } _functools_state; static inline _functools_state * @@ -1138,7 +1139,8 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds static PyObject * lru_cache_new(PyTypeObject *type, PyObject *args, PyObject *kw) { - PyObject *func, *maxsize_O, *cache_info_type, *cachedict; + PyObject *func, *maxsize_O, *cache_info_type, *cachedict, *obj_dict; + PyObject *defaults_str; int typed; lru_cache_object *obj; Py_ssize_t maxsize; @@ -1188,9 +1190,41 @@ lru_cache_new(PyTypeObject *type, PyObject *args, PyObject *kw) if (!(cachedict = PyDict_New())) return NULL; + obj_dict = PyDict_New(); + if (obj_dict == NULL) { + Py_DECREF(cachedict); + return NULL; + } + + /* Copy special attributes from the original function over to ours. */ + for (Py_ssize_t idx = 0; + idx < PyTuple_GET_SIZE(state->lru_obj_attrs_to_clone); + ++idx) { + PyObject *attr_name = PyTuple_GET_ITEM(state->lru_obj_attrs_to_clone, idx); + if (attr_name == NULL) { + Py_DECREF(cachedict); + Py_DECREF(obj_dict); + return NULL; + } + PyObject *attr = PyObject_GetAttr(func, attr_name); + if (attr != NULL) { + if (PyDict_SetItem(obj_dict, attr_name, attr) != 0) { + Py_DECREF(attr); + Py_DECREF(cachedict); + Py_DECREF(obj_dict); + return NULL; + } + Py_DECREF(attr); + } else { + /* The wrapped object didn't have attribute attr_name. */ + PyErr_Clear(); + } + } + obj = (lru_cache_object *)type->tp_alloc(type, 0); if (obj == NULL) { Py_DECREF(cachedict); + Py_DECREF(obj_dict); return NULL; } @@ -1209,7 +1243,7 @@ lru_cache_new(PyTypeObject *type, PyObject *args, PyObject *kw) obj->lru_list_elem_type = state->lru_list_elem_type; Py_INCREF(cache_info_type); obj->cache_info_type = cache_info_type; - obj->dict = NULL; + obj->dict = obj_dict; obj->weakreflist = NULL; return (PyObject *)obj; } @@ -1421,6 +1455,23 @@ static int _functools_exec(PyObject *module) { _functools_state *state = get_functools_state(module); + state->lru_obj_attrs_to_clone = PyTuple_New(2); + if (state->lru_obj_attrs_to_clone == NULL) { + return -1; + } + { + PyObject *tmp; + PyObject *lru_attrs = state->lru_obj_attrs_to_clone; + tmp = PyUnicode_InternFromString("__defaults__"); + if (tmp == NULL || PyTuple_SetItem(lru_attrs, 0, tmp) != 0) { // steal + return -1; + } + tmp = PyUnicode_InternFromString("__kwdefaults__"); + if (tmp == NULL || PyTuple_SetItem(lru_attrs, 1, tmp) != 0) { // steal + return -1; + } + } + state->kwd_mark = _PyObject_CallNoArg((PyObject *)&PyBaseObject_Type); if (state->kwd_mark == NULL) { return -1; @@ -1475,6 +1526,7 @@ _functools_traverse(PyObject *module, visitproc visit, void *arg) Py_VISIT(state->partial_type); Py_VISIT(state->keyobject_type); Py_VISIT(state->lru_list_elem_type); + Py_VISIT(state->lru_obj_attrs_to_clone); return 0; } @@ -1486,6 +1538,7 @@ _functools_clear(PyObject *module) Py_CLEAR(state->partial_type); Py_CLEAR(state->keyobject_type); Py_CLEAR(state->lru_list_elem_type); + Py_CLEAR(state->lru_obj_attrs_to_clone); return 0; }