Skip to content

Commit 18c7f37

Browse files
committed
fixes
1 parent acc636d commit 18c7f37

2 files changed

Lines changed: 61 additions & 39 deletions

File tree

Lib/functools.py

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,6 @@ def __setstate__(self, state):
436436
pass
437437

438438

439-
_PM_WRAPPED_METHOD_TYPES = (staticmethod, classmethod)
440-
_PM_FAST_METHOD_TYPES = _PM_WRAPPED_METHOD_TYPES + (FunctionType, partial)
441-
442-
443439
# Descriptor version
444440
class partialmethod:
445441
"""Method descriptor with partial application of the given arguments
@@ -450,7 +446,7 @@ class partialmethod:
450446
"""
451447
__slots__ = (
452448
"_func", "_args", "_keywords", "__dict__", "__weakref__",
453-
"_cachedmethod", "_iscacheable"
449+
"_cachedmethod", "_cachetype"
454450
)
455451

456452
__repr__ = _partial_repr
@@ -477,15 +473,19 @@ def func(self):
477473

478474
@func.setter
479475
def func(self, func):
480-
if isinstance(func, _PM_FAST_METHOD_TYPES):
481-
self._iscacheable = True
476+
if isinstance(func, staticmethod):
477+
self._cachetype = 1
478+
elif isinstance(func, classmethod):
479+
self._cachetype = 2
480+
elif isinstance(func, (FunctionType, partial)):
481+
self._cachetype = 3
482482
elif getattr(func, '__get__', None) is None:
483483
if not callable(func):
484484
raise TypeError(f'the first argument {func!r} must be a callable '
485485
'or a descriptor')
486-
self._iscacheable = True
486+
self._cachetype = 4
487487
else:
488-
self._iscacheable = False
488+
self._cachetype = 0
489489
self._func = func
490490
self._cachedmethod = None
491491

@@ -503,7 +503,8 @@ def keywords(self):
503503
method = self._cachedmethod
504504
if method is None:
505505
return self._keywords
506-
if isinstance(method, _PM_WRAPPED_METHOD_TYPES):
506+
cachetype = self._cachetype
507+
if cachetype in (1, 2):
507508
method = method.__wrapped__
508509
return method.keywords
509510

@@ -516,47 +517,48 @@ def __make_method(self):
516517
func = self._func
517518
args = self._args
518519
keywords = self._keywords
519-
if isinstance(func, staticmethod):
520-
deco = staticmethod
520+
cachetype = self._cachetype
521+
if cachetype == 1:
521522
method = partial(func.__wrapped__, *args, **keywords)
522-
elif isinstance(func, classmethod):
523-
deco = classmethod
523+
method.__isabstractmethod__ = self.__isabstractmethod__
524+
return staticmethod(method)
525+
elif cachetype == 2:
524526
ph_args = (Placeholder,) if args else ()
525527
method = partial(func.__wrapped__, *ph_args, *args, **keywords)
528+
method.__isabstractmethod__ = self.__isabstractmethod__
529+
return classmethod(method)
526530
else:
527-
# instance method. 2 cases:
528-
# a) FunctionType | partial
531+
# Either:
532+
# a) FunctionType / partial
529533
# b) callable object without __get__
530-
deco = None
534+
# c) non cacheable callable with __get__ that returned itself
531535
ph_args = (Placeholder,) if args else ()
532536
method = partial(func, *ph_args, *args, **keywords)
533-
534-
method.__partialmethod__ = self
535-
if self.__isabstractmethod__:
536-
method.__isabstractmethod__ = True
537-
if deco is not None:
538-
method = deco(method)
539-
return method
537+
method.__isabstractmethod__ = self.__isabstractmethod__
538+
method.__partialmethod__ = self
539+
return method
540540

541541
def __get__(self, obj, cls=None):
542+
if not self._cachetype:
543+
# Unknown descriptor == unknown binding
544+
# Get callable at runtime and apply partial on top
545+
func = self._func
546+
new_func = func.__get__(obj, cls)
547+
if new_func is not func:
548+
# Assume __get__ returning something new indicates the
549+
# creation of an appropriate callable
550+
result = partial(new_func, *self._args, **self._keywords)
551+
try:
552+
result.__self__ = new_func.__self__
553+
except AttributeError:
554+
pass
555+
return result
556+
else:
557+
return self.__make_method().__get__(obj, cls)
558+
542559
method = self._cachedmethod
543560
if method is None:
544-
if not self._iscacheable:
545-
# Unknown descriptor == unknown binding
546-
# Need to get callable at runtime and apply partial on top
547-
func = self._func
548-
new_func = func.__get__(obj, cls)
549-
if new_func is not func:
550-
result = partial(new_func, *self._args, **self._keywords)
551-
try:
552-
result.__self__ = new_func.__self__
553-
except AttributeError:
554-
pass
555-
return result
556-
557-
# Cache method
558561
self._cachedmethod = method = self.__make_method()
559-
560562
return method.__get__(obj, cls)
561563

562564
@property

Lib/test/test_functools.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,26 @@ def test_descriptors(self):
797797
self.assertEqual(obj.cls(c=8), ((self.A,), {'c': 8, 'd': 9}))
798798
self.assertEqual(obj.cls(5, c=8), ((self.A, 5), {'c': 8, 'd': 9}))
799799

800+
def test_descriptor_custom_bind(self):
801+
# make sure always returns bound branch
802+
class Desc:
803+
def __get__(self, obj, cls=None):
804+
if obj is None:
805+
return self
806+
return lambda a, b: ("bound", a, b)
807+
def __call__(self, *args):
808+
return ("Desc.__call__", args)
809+
810+
class A:
811+
pd = functools.partialmethod(Desc(), 1)
812+
813+
a = A()
814+
self.assertEqual(a.pd(2), ('bound', 1, 2))
815+
# This was broken by initial caching implementation
816+
# after any access
817+
A.pd
818+
self.assertEqual(a.pd(2), ('bound', 1, 2))
819+
800820
def test_overriding_keywords(self):
801821
self.assertEqual(self.a.keywords(a=3), ((self.a,), {'a': 3}))
802822
self.assertEqual(self.A.keywords(self.a, a=3), ((self.a,), {'a': 3}))

0 commit comments

Comments
 (0)