From 14adbd45980f705cb6554ca17b8a66b56e105296 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Sat, 20 Apr 2019 07:20:44 -1000 Subject: [PATCH] bpo-36650: Fix handling of empty keyword args in C version of lru_cache. (GH-12881) --- Lib/test/test_functools.py | 14 ++++++++++++++ .../2019-04-19-15-29-55.bpo-36650._EVdrz.rst | 4 ++++ Modules/_functoolsmodule.c | 7 +++---- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-04-19-15-29-55.bpo-36650._EVdrz.rst diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 4b2b9ab61fa..98908405e14 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1271,6 +1271,20 @@ class TestLRU: self.assertEqual(f(20), '.20.') self.assertEqual(f.cache_info().currsize, 10) + def test_lru_bug_36650(self): + # C version of lru_cache was treating a call with an empty **kwargs + # dictionary as being distinct from a call with no keywords at all. + # This did not result in an incorrect answer, but it did trigger + # an unexpected cache miss. + + @self.module.lru_cache() + def f(x): + pass + + f(0) + f(0, **{}) + self.assertEqual(f.cache_info().hits, 1) + def test_lru_hash_only_once(self): # To protect against weird reentrancy bugs and to improve # efficiency when faced with slow __hash__ methods, the diff --git a/Misc/NEWS.d/next/Library/2019-04-19-15-29-55.bpo-36650._EVdrz.rst b/Misc/NEWS.d/next/Library/2019-04-19-15-29-55.bpo-36650._EVdrz.rst new file mode 100644 index 00000000000..de10575fc27 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-19-15-29-55.bpo-36650._EVdrz.rst @@ -0,0 +1,4 @@ +The C version of functools.lru_cache() was treating calls with an empty +``**kwargs`` dictionary as being distinct from calls with no keywords at all. +This did not result in an incorrect answer, but it did trigger an unexpected +cache miss. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 3f1c01651de..dcc9129fc6b 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -750,8 +750,10 @@ lru_cache_make_key(PyObject *args, PyObject *kwds, int typed) PyObject *key, *keyword, *value; Py_ssize_t key_size, pos, key_pos, kwds_size; + kwds_size = kwds ? PyDict_GET_SIZE(kwds) : 0; + /* short path, key will match args anyway, which is a tuple */ - if (!typed && !kwds) { + if (!typed && !kwds_size) { if (PyTuple_GET_SIZE(args) == 1) { key = PyTuple_GET_ITEM(args, 0); if (PyUnicode_CheckExact(key) || PyLong_CheckExact(key)) { @@ -765,9 +767,6 @@ lru_cache_make_key(PyObject *args, PyObject *kwds, int typed) return args; } - kwds_size = kwds ? PyDict_GET_SIZE(kwds) : 0; - assert(kwds_size >= 0); - key_size = PyTuple_GET_SIZE(args); if (kwds_size) key_size += kwds_size * 2 + 1;