From 9d8a4fda9a7795a71b73758a0bd007d3a41ceea0 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 31 Oct 2021 10:59:32 +0300 Subject: [PATCH 1/4] bpo-45701: add `tuple` tests with `lru_cache` to `test_functools` --- Lib/test/test_functools.py | 53 +++++++++++++++++++ .../2021-10-31-10-58-45.bpo-45701.r0LAUL.rst | 2 + 2 files changed, 55 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2021-10-31-10-58-45.bpo-45701.r0LAUL.rst diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index bdb4ddcc60cac2b..b699d35f55cc8d6 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1495,6 +1495,59 @@ def square(x): self.assertEqual(square.cache_info().hits, 4) self.assertEqual(square.cache_info().misses, 4) + def test_lru_with_container_types(self): + def identity(x): + return x + + values = [(), (1, 2), (1, 2, 'a')] + for maxsize in (None, 128): + for current_value in values: + with self.subTest(current_value=current_value, maxsize=maxsize): + cached = self.module.lru_cache(maxsize, typed=True)(identity) + + # some unrelated tuple: + cached((1, 2, 3, 4, 5)) # miss + cached((1, 2, 3, 4, 5)) # hit + + cached(current_value) # miss + res = cached(current_value) # hit + + self.assertEqual(res, current_value) + self.assertEqual(cached.cache_info().hits, 2) + self.assertEqual(cached.cache_info().misses, 2) + + def test_lru_with_container_types_hash_collision(self): + # https://bugs.python.org/issue45701 + def get_zeroth(x): + return x[0] + + values = [ + # All values inside each tuple have the same hash: + # `hash(1) == hash(1.0) == hash(True)` + (0, 0.0, False), + (1, 1.0, True), + ] + for maxsize in (None, 128): + for hash_collision in values: + with self.subTest(maxsize=maxsize, values=values): + cached = self.module.lru_cache( + maxsize, + typed=True, + )(get_zeroth) + + # All these calls will be cached, because hash is the same. + self.assertEqual(type(hash_collision[0]), int) + self.assertEqual( # miss, cache created + type(cached((hash_collision[0], 2))), + int, + ) + + for value in hash_collision: # 3 hits + self.assertEqual(type(cached((value, 2))), int) + + self.assertEqual(cached.cache_info().hits, 3) + self.assertEqual(cached.cache_info().misses, 1) + def test_lru_with_keyword_args(self): @self.module.lru_cache() def fib(n): diff --git a/Misc/NEWS.d/next/Tests/2021-10-31-10-58-45.bpo-45701.r0LAUL.rst b/Misc/NEWS.d/next/Tests/2021-10-31-10-58-45.bpo-45701.r0LAUL.rst new file mode 100644 index 000000000000000..89106cf0660f09c --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2021-10-31-10-58-45.bpo-45701.r0LAUL.rst @@ -0,0 +1,2 @@ +Add tests with ``tuple`` type with :func:`functools.lru_cache` to +``test_functools``. From ea0968ff63124f891906bdcb91ace6a00c87b6d1 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 4 Nov 2021 10:54:30 +0300 Subject: [PATCH 2/4] Simplifies tests a bit --- Lib/test/test_functools.py | 79 +++++++++++++++----------------------- 1 file changed, 30 insertions(+), 49 deletions(-) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index b699d35f55cc8d6..1aeba8db599fceb 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1495,58 +1495,39 @@ def square(x): self.assertEqual(square.cache_info().hits, 4) self.assertEqual(square.cache_info().misses, 4) - def test_lru_with_container_types(self): - def identity(x): - return x - - values = [(), (1, 2), (1, 2, 'a')] - for maxsize in (None, 128): - for current_value in values: - with self.subTest(current_value=current_value, maxsize=maxsize): - cached = self.module.lru_cache(maxsize, typed=True)(identity) - - # some unrelated tuple: - cached((1, 2, 3, 4, 5)) # miss - cached((1, 2, 3, 4, 5)) # hit - - cached(current_value) # miss - res = cached(current_value) # hit - - self.assertEqual(res, current_value) - self.assertEqual(cached.cache_info().hits, 2) - self.assertEqual(cached.cache_info().misses, 2) - def test_lru_with_container_types_hash_collision(self): - # https://bugs.python.org/issue45701 - def get_zeroth(x): - return x[0] - - values = [ + """Testing hash collisions in `lru_cache(typed=True)`. + + Some different values in python have the same hash. + Like `0`, `False` and `0.0`. + When used in `tuple`, hash will still be the same: + `hash((1,)) == hash((True,))` + + The thing is, `typed=True` won't help in this case. + The first tuple with hash collisions will be used. + Context: https://bugs.python.org/issue45701 + """ + values = { # All values inside each tuple have the same hash: # `hash(1) == hash(1.0) == hash(True)` - (0, 0.0, False), - (1, 1.0, True), - ] - for maxsize in (None, 128): - for hash_collision in values: - with self.subTest(maxsize=maxsize, values=values): - cached = self.module.lru_cache( - maxsize, - typed=True, - )(get_zeroth) - - # All these calls will be cached, because hash is the same. - self.assertEqual(type(hash_collision[0]), int) - self.assertEqual( # miss, cache created - type(cached((hash_collision[0], 2))), - int, - ) - - for value in hash_collision: # 3 hits - self.assertEqual(type(cached((value, 2))), int) - - self.assertEqual(cached.cache_info().hits, 3) - self.assertEqual(cached.cache_info().misses, 1) + '(0,)': (0, 0.0, False), + '(1,)': (1, 1.0, True), + } + for expected, hash_collisions in values.items(): + with self.subTest(values=values): + cached = self.module.lru_cache(typed=True)(repr) + + # All these calls will be cached, because hash is the same. + self.assertEqual( # miss, cache created + cached((hash_collisions[0],)), + expected, + ) + + for value in hash_collisions: # 3 hits + self.assertEqual(cached((value,)), expected) + + self.assertEqual(cached.cache_info().hits, 3) + self.assertEqual(cached.cache_info().misses, 1) def test_lru_with_keyword_args(self): @self.module.lru_cache() From f2898817f9a459e0c2bcbc01f91c2c349364a8c8 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Nov 2021 16:32:55 +0300 Subject: [PATCH 3/4] Simplify test even more! --- Lib/test/test_functools.py | 50 +++++++++++++------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 1aeba8db599fceb..3f5cce5e0225acf 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1495,39 +1495,23 @@ def square(x): self.assertEqual(square.cache_info().hits, 4) self.assertEqual(square.cache_info().misses, 4) - def test_lru_with_container_types_hash_collision(self): - """Testing hash collisions in `lru_cache(typed=True)`. - - Some different values in python have the same hash. - Like `0`, `False` and `0.0`. - When used in `tuple`, hash will still be the same: - `hash((1,)) == hash((True,))` - - The thing is, `typed=True` won't help in this case. - The first tuple with hash collisions will be used. - Context: https://bugs.python.org/issue45701 - """ - values = { - # All values inside each tuple have the same hash: - # `hash(1) == hash(1.0) == hash(True)` - '(0,)': (0, 0.0, False), - '(1,)': (1, 1.0, True), - } - for expected, hash_collisions in values.items(): - with self.subTest(values=values): - cached = self.module.lru_cache(typed=True)(repr) - - # All these calls will be cached, because hash is the same. - self.assertEqual( # miss, cache created - cached((hash_collisions[0],)), - expected, - ) - - for value in hash_collisions: # 3 hits - self.assertEqual(cached((value,)), expected) - - self.assertEqual(cached.cache_info().hits, 3) - self.assertEqual(cached.cache_info().misses, 1) + def test_lru_cache_typed_is_not_recursive(self): + cached = self.module.lru_cache(typed=True)(repr) + + self.assertEqual(cached(1), '1') + self.assertEqual(cached(True), 'True') + self.assertEqual(cached(1.0), '1.0') + self.assertEqual(cached(0), '0') + self.assertEqual(cached(False), 'False') + self.assertEqual(cached(0.0), '0.0') + + self.assertEqual(cached((1,)), '(1,)') + self.assertEqual(cached((True,)), '(1,)') + self.assertEqual(cached((1.0,)), '(1,)') + + self.assertEqual(cached((0,)), '(0,)') + self.assertEqual(cached((False,)), '(0,)') + self.assertEqual(cached((0.0,)), '(0,)') def test_lru_with_keyword_args(self): @self.module.lru_cache() From 4fea891123506ab412165308b23b60cd1fe27d91 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Nov 2021 16:34:58 +0300 Subject: [PATCH 4/4] Add custom `tuple` subtype --- Lib/test/test_functools.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 3f5cce5e0225acf..1519bfac4143c51 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1508,11 +1508,20 @@ def test_lru_cache_typed_is_not_recursive(self): self.assertEqual(cached((1,)), '(1,)') self.assertEqual(cached((True,)), '(1,)') self.assertEqual(cached((1.0,)), '(1,)') - self.assertEqual(cached((0,)), '(0,)') self.assertEqual(cached((False,)), '(0,)') self.assertEqual(cached((0.0,)), '(0,)') + class T(tuple): + pass + + self.assertEqual(cached(T((1,))), '(1,)') + self.assertEqual(cached(T((True,))), '(1,)') + self.assertEqual(cached(T((1.0,))), '(1,)') + self.assertEqual(cached(T((0,))), '(0,)') + self.assertEqual(cached(T((False,))), '(0,)') + self.assertEqual(cached(T((0.0,))), '(0,)') + def test_lru_with_keyword_args(self): @self.module.lru_cache() def fib(n):