Skip to content

Commit 395335d

Browse files
gh-127750: Fix and optimize functools.singledispatchmethod() (GH-130008)
Remove broken singledispatchmethod caching introduced in gh-85160. Achieve the same performance using different optimization. * Add more tests. * Fix issues with __module__ and __doc__ descriptors.
1 parent fb2d325 commit 395335d

File tree

3 files changed

+102
-27
lines changed

3 files changed

+102
-27
lines changed

Lib/functools.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,9 +1026,6 @@ def __init__(self, func):
10261026
self.dispatcher = singledispatch(func)
10271027
self.func = func
10281028

1029-
import weakref # see comment in singledispatch function
1030-
self._method_cache = weakref.WeakKeyDictionary()
1031-
10321029
def register(self, cls, method=None):
10331030
"""generic_method.register(cls, func) -> func
10341031
@@ -1037,36 +1034,50 @@ def register(self, cls, method=None):
10371034
return self.dispatcher.register(cls, func=method)
10381035

10391036
def __get__(self, obj, cls=None):
1040-
if self._method_cache is not None:
1041-
try:
1042-
_method = self._method_cache[obj]
1043-
except TypeError:
1044-
self._method_cache = None
1045-
except KeyError:
1046-
pass
1047-
else:
1048-
return _method
1037+
return _singledispatchmethod_get(self, obj, cls)
10491038

1050-
dispatch = self.dispatcher.dispatch
1051-
funcname = getattr(self.func, '__name__', 'singledispatchmethod method')
1052-
def _method(*args, **kwargs):
1053-
if not args:
1054-
raise TypeError(f'{funcname} requires at least '
1055-
'1 positional argument')
1056-
return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs)
1039+
@property
1040+
def __isabstractmethod__(self):
1041+
return getattr(self.func, '__isabstractmethod__', False)
10571042

1058-
_method.__isabstractmethod__ = self.__isabstractmethod__
1059-
_method.register = self.register
1060-
update_wrapper(_method, self.func)
10611043

1062-
if self._method_cache is not None:
1063-
self._method_cache[obj] = _method
1044+
class _singledispatchmethod_get:
1045+
def __init__(self, unbound, obj, cls):
1046+
self._unbound = unbound
1047+
self._dispatch = unbound.dispatcher.dispatch
1048+
self._obj = obj
1049+
self._cls = cls
1050+
# Set instance attributes which cannot be handled in __getattr__()
1051+
# because they conflict with type descriptors.
1052+
func = unbound.func
1053+
try:
1054+
self.__module__ = func.__module__
1055+
except AttributeError:
1056+
pass
1057+
try:
1058+
self.__doc__ = func.__doc__
1059+
except AttributeError:
1060+
pass
1061+
1062+
def __call__(self, /, *args, **kwargs):
1063+
if not args:
1064+
funcname = getattr(self._unbound.func, '__name__',
1065+
'singledispatchmethod method')
1066+
raise TypeError(f'{funcname} requires at least '
1067+
'1 positional argument')
1068+
return self._dispatch(args[0].__class__).__get__(self._obj, self._cls)(*args, **kwargs)
10641069

1065-
return _method
1070+
def __getattr__(self, name):
1071+
# Resolve these attributes lazily to speed up creation of
1072+
# the _singledispatchmethod_get instance.
1073+
if name not in {'__name__', '__qualname__', '__isabstractmethod__',
1074+
'__annotations__', '__type_params__'}:
1075+
raise AttributeError
1076+
return getattr(self._unbound.func, name)
10661077

10671078
@property
1068-
def __isabstractmethod__(self):
1069-
return getattr(self.func, '__isabstractmethod__', False)
1079+
def register(self):
1080+
return self._unbound.register
10701081

10711082

10721083
################################################################################

Lib/test/test_functools.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2910,6 +2910,7 @@ def static_func(arg: int) -> str:
29102910
"""My function docstring"""
29112911
return str(arg)
29122912

2913+
prefix = A.__qualname__ + '.'
29132914
for meth in (
29142915
A.func,
29152916
A().func,
@@ -2919,6 +2920,9 @@ def static_func(arg: int) -> str:
29192920
A().static_func
29202921
):
29212922
with self.subTest(meth=meth):
2923+
self.assertEqual(meth.__module__, __name__)
2924+
self.assertEqual(type(meth).__module__, 'functools')
2925+
self.assertEqual(meth.__qualname__, prefix + meth.__name__)
29222926
self.assertEqual(meth.__doc__,
29232927
('My function docstring'
29242928
if support.HAVE_DOCSTRINGS
@@ -3251,6 +3255,64 @@ def f(arg):
32513255
def _(arg: undefined):
32523256
return "forward reference"
32533257

3258+
def test_method_equal_instances(self):
3259+
# gh-127750: Reference to self was cached
3260+
class A:
3261+
def __eq__(self, other):
3262+
return True
3263+
def __hash__(self):
3264+
return 1
3265+
@functools.singledispatchmethod
3266+
def t(self, arg):
3267+
return self
3268+
3269+
a = A()
3270+
b = A()
3271+
self.assertIs(a.t(1), a)
3272+
self.assertIs(b.t(2), b)
3273+
3274+
def test_method_bad_hash(self):
3275+
class A:
3276+
def __eq__(self, other):
3277+
raise AssertionError
3278+
def __hash__(self):
3279+
raise AssertionError
3280+
@functools.singledispatchmethod
3281+
def t(self, arg):
3282+
pass
3283+
3284+
# Should not raise
3285+
A().t(1)
3286+
hash(A().t)
3287+
A().t == A().t
3288+
3289+
def test_method_no_reference_loops(self):
3290+
# gh-127750: Created a strong reference to self
3291+
class A:
3292+
@functools.singledispatchmethod
3293+
def t(self, arg):
3294+
return weakref.ref(self)
3295+
3296+
a = A()
3297+
r = a.t(1)
3298+
self.assertIsNotNone(r())
3299+
del a # delete a after a.t
3300+
if not support.check_impl_detail(cpython=True):
3301+
support.gc_collect()
3302+
self.assertIsNone(r())
3303+
3304+
a = A()
3305+
t = a.t
3306+
del a # delete a before a.t
3307+
support.gc_collect()
3308+
r = t(1)
3309+
self.assertIsNotNone(r())
3310+
del t
3311+
if not support.check_impl_detail(cpython=True):
3312+
support.gc_collect()
3313+
self.assertIsNone(r())
3314+
3315+
32543316
class CachedCostItem:
32553317
_cost = 1
32563318

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Remove broken :func:`functools.singledispatchmethod` caching introduced in
2+
:gh:`85160`. Achieve the same performance using different optimization.

0 commit comments

Comments
 (0)