diff --git a/Lib/functools.py b/Lib/functools.py index a92844ba7227b0..c80ac7b380801b 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -14,12 +14,12 @@ 'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod', 'cached_property', 'Placeholder'] -from abc import get_cache_token +from abc import abstractmethod, get_cache_token from collections import namedtuple # import weakref # Deferred to single_dispatch() from operator import itemgetter from reprlib import recursive_repr -from types import GenericAlias, MethodType, MappingProxyType, UnionType +from types import FunctionType, GenericAlias, MethodType, MappingProxyType, UnionType from _thread import RLock ################################################################################ @@ -310,52 +310,6 @@ def _partial_prepare_merger(args): merger = itemgetter(*order) if phcount else None return phcount, merger -def _partial_new(cls, func, /, *args, **keywords): - if issubclass(cls, partial): - base_cls = partial - if not callable(func): - raise TypeError("the first argument must be callable") - else: - base_cls = partialmethod - # func could be a descriptor like classmethod which isn't callable - if not callable(func) and not hasattr(func, "__get__"): - raise TypeError(f"the first argument {func!r} must be a callable " - "or a descriptor") - if args and args[-1] is Placeholder: - raise TypeError("trailing Placeholders are not allowed") - for value in keywords.values(): - if value is Placeholder: - raise TypeError("Placeholder cannot be passed as a keyword argument") - if isinstance(func, base_cls): - pto_phcount = func._phcount - tot_args = func.args - if args: - tot_args += args - if pto_phcount: - # merge args with args of `func` which is `partial` - nargs = len(args) - if nargs < pto_phcount: - tot_args += (Placeholder,) * (pto_phcount - nargs) - tot_args = func._merger(tot_args) - if nargs > pto_phcount: - tot_args += args[pto_phcount:] - phcount, merger = _partial_prepare_merger(tot_args) - else: # works for both pto_phcount == 0 and != 0 - phcount, merger = pto_phcount, func._merger - keywords = {**func.keywords, **keywords} - func = func.func - else: - tot_args = args - phcount, merger = _partial_prepare_merger(tot_args) - - self = object.__new__(cls) - self.func = func - self.args = tot_args - self.keywords = keywords - self._phcount = phcount - self._merger = merger - return self - def _partial_repr(self): cls = type(self) module = cls.__module__ @@ -374,7 +328,44 @@ class partial: __slots__ = ("func", "args", "keywords", "_phcount", "_merger", "__dict__", "__weakref__") - __new__ = _partial_new + def __new__(cls, func, /, *args, **keywords): + if not callable(func): + raise TypeError("the first argument must be callable") + if args and args[-1] is Placeholder: + raise TypeError("trailing Placeholders are not allowed") + for value in keywords.values(): + if value is Placeholder: + raise TypeError("Placeholder cannot be passed as a keyword argument") + if isinstance(func, partial): + pto_phcount = func._phcount + tot_args = func.args + if args: + tot_args += args + if pto_phcount: + # merge args with args of `func` which is `partial` + nargs = len(args) + if nargs < pto_phcount: + tot_args += (Placeholder,) * (pto_phcount - nargs) + tot_args = func._merger(tot_args) + if nargs > pto_phcount: + tot_args += args[pto_phcount:] + phcount, merger = _partial_prepare_merger(tot_args) + else: # works for both pto_phcount == 0 and != 0 + phcount, merger = pto_phcount, func._merger + keywords = {**func.keywords, **keywords} + func = func.func + else: + tot_args = args + phcount, merger = _partial_prepare_merger(tot_args) + + self = object.__new__(cls) + self.func = func + self.args = tot_args + self.keywords = keywords + self._phcount = phcount + self._merger = merger + return self + __repr__ = recursive_repr()(_partial_repr) def __call__(self, /, *args, **keywords): @@ -439,6 +430,12 @@ def __setstate__(self, state): except ImportError: pass +_UNKNOWN_DESCRIPTOR = object() +_STD_METHOD_TYPES = (staticmethod, classmethod, FunctionType, partial) +_ONE_PLACEHOLDER_TUPLE = (Placeholder,) +_VOID_LAMBDA = lambda *_, **__: None + + # Descriptor version class partialmethod: """Method descriptor with partial application of the given arguments @@ -447,46 +444,83 @@ class partialmethod: Supports wrapping existing descriptors and handles non-descriptor callables as instance methods. """ - __new__ = _partial_new + + __slots__ = ("func", "args", "keywords", "wrapper", + "__dict__", "__weakref__") + __repr__ = _partial_repr - def _make_unbound_method(self): - def _method(cls_or_self, /, *args, **keywords): - phcount = self._phcount - if phcount: - try: - pto_args = self._merger(self.args + args) - args = args[phcount:] - except IndexError: - raise TypeError("missing positional arguments " - "in 'partialmethod' call; expected " - f"at least {phcount}, got {len(args)}") - else: - pto_args = self.args - keywords = {**self.keywords, **keywords} - return self.func(cls_or_self, *pto_args, *args, **keywords) - _method.__isabstractmethod__ = self.__isabstractmethod__ - _method.__partialmethod__ = self - return _method + def __init__(self, func, /, *args, **keywords): + if isinstance(func, partialmethod): + # Subclass optimization + temp = partial(_VOID_LAMBDA, *func.args, **func.keywords) + temp = partial(temp, *args, **keywords) + func = func.func + args = temp.args + keywords = temp.keywords + + self.func = func + self.args = args + self.keywords = keywords + + if isinstance(func, _STD_METHOD_TYPES): + self.method = None + elif getattr(func, '__get__', None) is None: + if not callable(func): + raise TypeError(f'the first argument {func!r} must be a callable ' + 'or a descriptor') + self.method = None + else: + # Unknown descriptor + self.method = _UNKNOWN_DESCRIPTOR + + def __make_method(self): + args = self.args + func = self.func + + if isinstance(func, staticmethod): + deco = staticmethod + method = partial(func.__wrapped__, *args, **self.keywords) + elif isinstance(func, classmethod): + deco = classmethod + ph_args = _ONE_PLACEHOLDER_TUPLE if args else () + method = partial(func.__wrapped__, *ph_args, *args, **self.keywords) + else: + # instance method. 2 cases: + # a) FunctionType | partial + # b) callable object without __get__ + deco = None + ph_args = _ONE_PLACEHOLDER_TUPLE if args else () + method = partial(func, *ph_args, *args, **self.keywords) + + method.__partialmethod__ = self + if self.__isabstractmethod__: + method = abstractmethod(method) + if deco is not None: + method = deco(method) + return method def __get__(self, obj, cls=None): - get = getattr(self.func, "__get__", None) - result = None - if get is not None: - new_func = get(obj, cls) - if new_func is not self.func: - # Assume __get__ returning something new indicates the - # creation of an appropriate callable - result = partial(new_func, *self.args, **self.keywords) - try: - result.__self__ = new_func.__self__ - except AttributeError: - pass - if result is None: - # If the underlying descriptor didn't do anything, treat this - # like an instance method - result = self._make_unbound_method().__get__(obj, cls) - return result + method = self.method + if method is _UNKNOWN_DESCRIPTOR: + # Unknown descriptor == unknown binding + # Need to get callable at runtime and apply partial on top + new_func = self.func.__get__(obj, cls) + result = partial(new_func, *self.args, **self.keywords) + result.__partialmethod__ = self + if self.__isabstractmethod__: + result = abstractmethod(result) + try: + obj = new_func.__self__ + except AttributeError: + pass + else: + result.__self__ = obj + return result + if method is None: + # Cache method + self.method = method = self.__make_method() + return method.__get__(obj, cls) @property def __isabstractmethod__(self): @@ -506,13 +540,14 @@ def _unwrap_partialmethod(func): prev = None while func is not prev: prev = func - while isinstance(getattr(func, "__partialmethod__", None), partialmethod): - func = func.__partialmethod__ - while isinstance(func, partialmethod): - func = getattr(func, 'func') - func = _unwrap_partial(func) + __partialmethod__ = getattr(func, "__partialmethod__", None) + if isinstance(__partialmethod__, partialmethod): + func = __partialmethod__.func + if isinstance(func, (partial, partialmethod)): + func = func.func return func + ################################################################################ ### LRU Cache function decorator ################################################################################ diff --git a/Lib/inspect.py b/Lib/inspect.py index 5a46987b78b437..64c14194196323 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2467,39 +2467,6 @@ def _signature_from_callable(obj, *, 'attribute'.format(sig)) return sig - try: - partialmethod = obj.__partialmethod__ - except AttributeError: - pass - else: - if isinstance(partialmethod, functools.partialmethod): - # Unbound partialmethod (see functools.partialmethod) - # This means, that we need to calculate the signature - # as if it's a regular partial object, but taking into - # account that the first positional argument - # (usually `self`, or `cls`) will not be passed - # automatically (as for boundmethods) - - wrapped_sig = _get_signature_of(partialmethod.func) - - sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) - first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] - if first_wrapped_param.kind is Parameter.VAR_POSITIONAL: - # First argument of the wrapped callable is `*args`, as in - # `partialmethod(lambda *args)`. - return sig - else: - sig_params = tuple(sig.parameters.values()) - assert (not sig_params or - first_wrapped_param is not sig_params[0]) - # If there were placeholders set, - # first param is transformed to positional only - if partialmethod.args.count(functools.Placeholder): - first_wrapped_param = first_wrapped_param.replace( - kind=Parameter.POSITIONAL_ONLY) - new_params = (first_wrapped_param,) + sig_params - return sig.replace(parameters=new_params) - if isinstance(obj, functools.partial): wrapped_sig = _get_signature_of(obj.func) return _signature_get_partial(wrapped_sig, obj) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 88fa4b7460c412..d4d8274cab6b8e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3757,8 +3757,10 @@ def test(): pass ham = partialmethod(test) - with self.assertRaisesRegex(ValueError, "has incorrect arguments"): - inspect.signature(Spam.ham) + self.assertEqual(self.signature(Spam.ham, eval_str=False), + ((), Ellipsis)) + with self.assertRaisesRegex(ValueError, "invalid method signature"): + inspect.signature(Spam().ham) class Spam: def test(it, a, b, *, c) -> 'spam': @@ -3797,7 +3799,7 @@ def test(self: 'anno', x): g = partialmethod(test, 1) self.assertEqual(self.signature(Spam.g, eval_str=False), - ((('self', ..., 'anno', 'positional_or_keyword'),), + ((('self', ..., 'anno', 'positional_only'),), ...)) def test_signature_on_fake_partialmethod(self): diff --git a/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst b/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst new file mode 100644 index 00000000000000..de3d57f39e3765 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst @@ -0,0 +1 @@ +:func:`functools.partialmethod` is now faster by making use of new :func:`functools.partial` placeholder functionality.