From 6c6e8b8d68d5de724bed365a735f844e906fba1a Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 11 Dec 2024 19:30:16 +0100 Subject: [PATCH 1/4] add singledispatchmethod test for objects with equal hash and == --- Lib/test/test_functools.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index ffd2adb8665b45..e6ef95624249a7 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3228,6 +3228,25 @@ def f(arg): def _(arg: undefined): return "forward reference" + def test_singledispatchmethod_hash_comparision_equal(self): + from dataclasses import dataclass + + @dataclass(frozen=True) + class A: + value: int + + @functools.singledispatchmethod + def dispatch(self, x): + return id(self) + + t1 = A(1) + t2 = A(1) + + assert t1 == t2 + assert id(t1) == t1.dispatch(2) + assert id(t2) == t2.dispatch(2) # gh-127750 + + class CachedCostItem: _cost = 1 From ef3f8c977d392c2f822f579d6695b562507b0b35 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 11 Dec 2024 19:44:17 +0100 Subject: [PATCH 2/4] add singledispatchmethod test for dangling references --- Lib/test/test_functools.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index e6ef95624249a7..d24294c7c8c837 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3,6 +3,7 @@ import collections import collections.abc import copy +import gc from itertools import permutations import pickle from random import choice @@ -3246,6 +3247,33 @@ def dispatch(self, x): assert id(t1) == t1.dispatch(2) assert id(t2) == t2.dispatch(2) # gh-127750 + def test_singledispatchmethod_object_references(self): + class ReferenceTest: + instance_counter = 0 + + def __init__(self): + ReferenceTest.instance_counter = ReferenceTest.instance_counter + 1 + + def __del__(self): + ReferenceTest.instance_counter = ReferenceTest.instance_counter - 1 + + @functools.singledispatchmethod + def go(self, item): + pass + + assert ReferenceTest.instance_counter == 0 + t=ReferenceTest() + assert ReferenceTest.instance_counter == 1 + x = [] + for ii in range(1000): + t = ReferenceTest() + t.go(ii) + x.append(t) + del t + del x + gc.collect() + assert ReferenceTest.instance_counter == 0 + class CachedCostItem: _cost = 1 From 460e309c5e4e9c4d1f3145f1b51f73879a9ed34f Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 11 Dec 2024 19:45:50 +0100 Subject: [PATCH 3/4] use id for object cache in singledispatchmethod --- Lib/functools.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index eff6540c7f606e..69f32bd59c4801 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -1029,7 +1029,7 @@ def __init__(self, func): self.func = func import weakref # see comment in singledispatch function - self._method_cache = weakref.WeakKeyDictionary() + self._method_cache = weakref.WeakValueDictionary() def register(self, cls, method=None): """generic_method.register(cls, func) -> func @@ -1039,15 +1039,12 @@ def register(self, cls, method=None): return self.dispatcher.register(cls, func=method) def __get__(self, obj, cls=None): - if self._method_cache is not None: - try: - _method = self._method_cache[obj] - except TypeError: - self._method_cache = None - except KeyError: - pass - else: - return _method + try: + _method = self._method_cache[id(obj)] + except KeyError: + pass + else: + return _method dispatch = self.dispatcher.dispatch funcname = getattr(self.func, '__name__', 'singledispatchmethod method') @@ -1057,12 +1054,12 @@ def _method(*args, **kwargs): '1 positional argument') return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs) + _method.__self_reference = _method # create strong reference to _method. see gh-127751 _method.__isabstractmethod__ = self.__isabstractmethod__ _method.register = self.register update_wrapper(_method, self.func) - if self._method_cache is not None: - self._method_cache[obj] = _method + self._method_cache[id(obj)] = _method return _method From 806e3c72af1d565ada34d91a3c9e543dc0f50e86 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:26:47 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-12-11-20-26-44.gh-issue-127750.8witaH.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-12-11-20-26-44.gh-issue-127750.8witaH.rst diff --git a/Misc/NEWS.d/next/Library/2024-12-11-20-26-44.gh-issue-127750.8witaH.rst b/Misc/NEWS.d/next/Library/2024-12-11-20-26-44.gh-issue-127750.8witaH.rst new file mode 100644 index 00000000000000..9ebf07e958e10d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-11-20-26-44.gh-issue-127750.8witaH.rst @@ -0,0 +1 @@ +Update caching of :class:`functools.singledispatchmethod` to avoid collisions of objects which compare equal.