Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-44003: expose default args on the wrapped functools.lru_cache function #25800

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -510,15 +512,15 @@ 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')

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

Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
57 changes: 55 additions & 2 deletions Modules/_functoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down