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 diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index ffd2adb8665b45..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 @@ -3228,6 +3229,52 @@ 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 + + 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 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.