mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
gh-119127: functools.partial placeholders (gh-119827)
This commit is contained in:
parent
4defb58d38
commit
d9296529eb
8 changed files with 682 additions and 130 deletions
|
@ -328,6 +328,14 @@ The :mod:`functools` module defines the following functions:
|
||||||
Returning ``NotImplemented`` from the underlying comparison function for
|
Returning ``NotImplemented`` from the underlying comparison function for
|
||||||
unrecognised types is now supported.
|
unrecognised types is now supported.
|
||||||
|
|
||||||
|
.. data:: Placeholder
|
||||||
|
|
||||||
|
A singleton object used as a sentinel to reserve a place
|
||||||
|
for positional arguments when calling :func:`partial`
|
||||||
|
and :func:`partialmethod`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
.. function:: partial(func, /, *args, **keywords)
|
.. function:: partial(func, /, *args, **keywords)
|
||||||
|
|
||||||
Return a new :ref:`partial object<partial-objects>` which when called
|
Return a new :ref:`partial object<partial-objects>` which when called
|
||||||
|
@ -338,26 +346,67 @@ The :mod:`functools` module defines the following functions:
|
||||||
Roughly equivalent to::
|
Roughly equivalent to::
|
||||||
|
|
||||||
def partial(func, /, *args, **keywords):
|
def partial(func, /, *args, **keywords):
|
||||||
def newfunc(*fargs, **fkeywords):
|
def newfunc(*more_args, **more_keywords):
|
||||||
newkeywords = {**keywords, **fkeywords}
|
keywords_union = {**keywords, **more_keywords}
|
||||||
return func(*args, *fargs, **newkeywords)
|
return func(*args, *more_args, **keywords_union)
|
||||||
newfunc.func = func
|
newfunc.func = func
|
||||||
newfunc.args = args
|
newfunc.args = args
|
||||||
newfunc.keywords = keywords
|
newfunc.keywords = keywords
|
||||||
return newfunc
|
return newfunc
|
||||||
|
|
||||||
The :func:`partial` is used for partial function application which "freezes"
|
The :func:`partial` function is used for partial function application which "freezes"
|
||||||
some portion of a function's arguments and/or keywords resulting in a new object
|
some portion of a function's arguments and/or keywords resulting in a new object
|
||||||
with a simplified signature. For example, :func:`partial` can be used to create
|
with a simplified signature. For example, :func:`partial` can be used to create
|
||||||
a callable that behaves like the :func:`int` function where the *base* argument
|
a callable that behaves like the :func:`int` function where the *base* argument
|
||||||
defaults to two:
|
defaults to ``2``:
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
>>> from functools import partial
|
|
||||||
>>> basetwo = partial(int, base=2)
|
>>> basetwo = partial(int, base=2)
|
||||||
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
|
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
|
||||||
>>> basetwo('10010')
|
>>> basetwo('10010')
|
||||||
18
|
18
|
||||||
|
|
||||||
|
If :data:`Placeholder` sentinels are present in *args*, they will be filled first
|
||||||
|
when :func:`partial` is called. This allows custom selection of positional arguments
|
||||||
|
to be pre-filled when constructing a :ref:`partial object <partial-objects>`.
|
||||||
|
|
||||||
|
If :data:`!Placeholder` sentinels are present, all of them must be filled at call time:
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> say_to_world = partial(print, Placeholder, Placeholder, "world!")
|
||||||
|
>>> say_to_world('Hello', 'dear')
|
||||||
|
Hello dear world!
|
||||||
|
|
||||||
|
Calling ``say_to_world('Hello')`` would raise a :exc:`TypeError`, because
|
||||||
|
only one positional argument is provided, while there are two placeholders
|
||||||
|
in :ref:`partial object <partial-objects>`.
|
||||||
|
|
||||||
|
Successive :func:`partial` applications fill :data:`!Placeholder` sentinels
|
||||||
|
of the input :func:`partial` objects with new positional arguments.
|
||||||
|
A place for positional argument can be retained by inserting new
|
||||||
|
:data:`!Placeholder` sentinel to the place held by previous :data:`!Placeholder`:
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from functools import partial, Placeholder as _
|
||||||
|
>>> remove = partial(str.replace, _, _, '')
|
||||||
|
>>> message = 'Hello, dear dear world!'
|
||||||
|
>>> remove(message, ' dear')
|
||||||
|
'Hello, world!'
|
||||||
|
>>> remove_dear = partial(remove, _, ' dear')
|
||||||
|
>>> remove_dear(message)
|
||||||
|
'Hello, world!'
|
||||||
|
>>> remove_first_dear = partial(remove_dear, _, 1)
|
||||||
|
>>> remove_first_dear(message)
|
||||||
|
'Hello, dear world!'
|
||||||
|
|
||||||
|
Note, :data:`!Placeholder` has no special treatment when used for keyword
|
||||||
|
argument of :data:`!Placeholder`.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.14
|
||||||
|
Added support for :data:`Placeholder` in positional arguments.
|
||||||
|
|
||||||
.. class:: partialmethod(func, /, *args, **keywords)
|
.. class:: partialmethod(func, /, *args, **keywords)
|
||||||
|
|
||||||
|
@ -742,10 +791,7 @@ have three read-only attributes:
|
||||||
The keyword arguments that will be supplied when the :class:`partial` object is
|
The keyword arguments that will be supplied when the :class:`partial` object is
|
||||||
called.
|
called.
|
||||||
|
|
||||||
:class:`partial` objects are like :ref:`function objects <user-defined-funcs>`
|
:class:`partial` objects are like :class:`function` objects in that they are
|
||||||
in that they are callable, weak referenceable, and can have attributes.
|
callable, weak referenceable, and can have attributes. There are some important
|
||||||
There are some important differences. For instance, the
|
differences. For instance, the :attr:`~definition.__name__` and :attr:`__doc__` attributes
|
||||||
:attr:`~function.__name__` and :attr:`function.__doc__` attributes
|
are not created automatically.
|
||||||
are not created automatically. Also, :class:`partial` objects defined in
|
|
||||||
classes behave like static methods and do not transform into bound methods
|
|
||||||
during instance attribute look-up.
|
|
||||||
|
|
|
@ -255,6 +255,15 @@ Added support for converting any objects that have the
|
||||||
(Contributed by Serhiy Storchaka in :gh:`82017`.)
|
(Contributed by Serhiy Storchaka in :gh:`82017`.)
|
||||||
|
|
||||||
|
|
||||||
|
functools
|
||||||
|
---------
|
||||||
|
|
||||||
|
* Added support to :func:`functools.partial` and
|
||||||
|
:func:`functools.partialmethod` for :data:`functools.Placeholder` sentinels
|
||||||
|
to reserve a place for positional arguments.
|
||||||
|
(Contributed by Dominykas Grigonis in :gh:`119127`.)
|
||||||
|
|
||||||
|
|
||||||
http
|
http
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
194
Lib/functools.py
194
Lib/functools.py
|
@ -6,17 +6,18 @@
|
||||||
# Written by Nick Coghlan <ncoghlan at gmail.com>,
|
# Written by Nick Coghlan <ncoghlan at gmail.com>,
|
||||||
# Raymond Hettinger <python at rcn.com>,
|
# Raymond Hettinger <python at rcn.com>,
|
||||||
# and Łukasz Langa <lukasz at langa.pl>.
|
# and Łukasz Langa <lukasz at langa.pl>.
|
||||||
# Copyright (C) 2006-2013 Python Software Foundation.
|
# Copyright (C) 2006-2024 Python Software Foundation.
|
||||||
# See C source code for _functools credits/copyright
|
# See C source code for _functools credits/copyright
|
||||||
|
|
||||||
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
|
__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
|
||||||
'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce',
|
'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce',
|
||||||
'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
|
'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
|
||||||
'cached_property']
|
'cached_property', 'Placeholder']
|
||||||
|
|
||||||
from abc import get_cache_token
|
from abc import get_cache_token
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
# import types, weakref # Deferred to single_dispatch()
|
# import types, weakref # Deferred to single_dispatch()
|
||||||
|
from operator import itemgetter
|
||||||
from reprlib import recursive_repr
|
from reprlib import recursive_repr
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
from _thread import RLock
|
from _thread import RLock
|
||||||
|
@ -274,43 +275,125 @@ except ImportError:
|
||||||
### partial() argument application
|
### partial() argument application
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class _PlaceholderType:
|
||||||
|
"""The type of the Placeholder singleton.
|
||||||
|
|
||||||
|
Used as a placeholder for partial arguments.
|
||||||
|
"""
|
||||||
|
__instance = None
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __init_subclass__(cls, *args, **kwargs):
|
||||||
|
raise TypeError(f"type '{cls.__name__}' is not an acceptable base type")
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls.__instance is None:
|
||||||
|
cls.__instance = object.__new__(cls)
|
||||||
|
return cls.__instance
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Placeholder'
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return 'Placeholder'
|
||||||
|
|
||||||
|
Placeholder = _PlaceholderType()
|
||||||
|
|
||||||
|
def _partial_prepare_merger(args):
|
||||||
|
if not args:
|
||||||
|
return 0, None
|
||||||
|
nargs = len(args)
|
||||||
|
order = []
|
||||||
|
j = nargs
|
||||||
|
for i, a in enumerate(args):
|
||||||
|
if a is Placeholder:
|
||||||
|
order.append(j)
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
order.append(i)
|
||||||
|
phcount = j - nargs
|
||||||
|
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")
|
||||||
|
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__
|
||||||
|
qualname = cls.__qualname__
|
||||||
|
args = [repr(self.func)]
|
||||||
|
args.extend(map(repr, self.args))
|
||||||
|
args.extend(f"{k}={v!r}" for k, v in self.keywords.items())
|
||||||
|
return f"{module}.{qualname}({', '.join(args)})"
|
||||||
|
|
||||||
# Purely functional, no descriptor behaviour
|
# Purely functional, no descriptor behaviour
|
||||||
class partial:
|
class partial:
|
||||||
"""New function with partial application of the given arguments
|
"""New function with partial application of the given arguments
|
||||||
and keywords.
|
and keywords.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
|
__slots__ = ("func", "args", "keywords", "_phcount", "_merger",
|
||||||
|
"__dict__", "__weakref__")
|
||||||
|
|
||||||
def __new__(cls, func, /, *args, **keywords):
|
__new__ = _partial_new
|
||||||
if not callable(func):
|
__repr__ = recursive_repr()(_partial_repr)
|
||||||
raise TypeError("the first argument must be callable")
|
|
||||||
|
|
||||||
if isinstance(func, partial):
|
|
||||||
args = func.args + args
|
|
||||||
keywords = {**func.keywords, **keywords}
|
|
||||||
func = func.func
|
|
||||||
|
|
||||||
self = super(partial, cls).__new__(cls)
|
|
||||||
|
|
||||||
self.func = func
|
|
||||||
self.args = args
|
|
||||||
self.keywords = keywords
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __call__(self, /, *args, **keywords):
|
def __call__(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 'partial' call; expected "
|
||||||
|
f"at least {phcount}, got {len(args)}")
|
||||||
|
else:
|
||||||
|
pto_args = self.args
|
||||||
keywords = {**self.keywords, **keywords}
|
keywords = {**self.keywords, **keywords}
|
||||||
return self.func(*self.args, *args, **keywords)
|
return self.func(*pto_args, *args, **keywords)
|
||||||
|
|
||||||
@recursive_repr()
|
|
||||||
def __repr__(self):
|
|
||||||
cls = type(self)
|
|
||||||
qualname = cls.__qualname__
|
|
||||||
module = cls.__module__
|
|
||||||
args = [repr(self.func)]
|
|
||||||
args.extend(repr(x) for x in self.args)
|
|
||||||
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
|
|
||||||
return f"{module}.{qualname}({', '.join(args)})"
|
|
||||||
|
|
||||||
def __get__(self, obj, objtype=None):
|
def __get__(self, obj, objtype=None):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
|
@ -332,6 +415,10 @@ class partial:
|
||||||
(namespace is not None and not isinstance(namespace, dict))):
|
(namespace is not None and not isinstance(namespace, dict))):
|
||||||
raise TypeError("invalid partial state")
|
raise TypeError("invalid partial state")
|
||||||
|
|
||||||
|
if args and args[-1] is Placeholder:
|
||||||
|
raise TypeError("trailing Placeholders are not allowed")
|
||||||
|
phcount, merger = _partial_prepare_merger(args)
|
||||||
|
|
||||||
args = tuple(args) # just in case it's a subclass
|
args = tuple(args) # just in case it's a subclass
|
||||||
if kwds is None:
|
if kwds is None:
|
||||||
kwds = {}
|
kwds = {}
|
||||||
|
@ -344,53 +431,40 @@ class partial:
|
||||||
self.func = func
|
self.func = func
|
||||||
self.args = args
|
self.args = args
|
||||||
self.keywords = kwds
|
self.keywords = kwds
|
||||||
|
self._phcount = phcount
|
||||||
|
self._merger = merger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from _functools import partial
|
from _functools import partial, Placeholder, _PlaceholderType
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Descriptor version
|
# Descriptor version
|
||||||
class partialmethod(object):
|
class partialmethod:
|
||||||
"""Method descriptor with partial application of the given arguments
|
"""Method descriptor with partial application of the given arguments
|
||||||
and keywords.
|
and keywords.
|
||||||
|
|
||||||
Supports wrapping existing descriptors and handles non-descriptor
|
Supports wrapping existing descriptors and handles non-descriptor
|
||||||
callables as instance methods.
|
callables as instance methods.
|
||||||
"""
|
"""
|
||||||
|
__new__ = _partial_new
|
||||||
def __init__(self, func, /, *args, **keywords):
|
__repr__ = _partial_repr
|
||||||
if not callable(func) and not hasattr(func, "__get__"):
|
|
||||||
raise TypeError("{!r} is not callable or a descriptor"
|
|
||||||
.format(func))
|
|
||||||
|
|
||||||
# func could be a descriptor like classmethod which isn't callable,
|
|
||||||
# so we can't inherit from partial (it verifies func is callable)
|
|
||||||
if isinstance(func, partialmethod):
|
|
||||||
# flattening is mandatory in order to place cls/self before all
|
|
||||||
# other arguments
|
|
||||||
# it's also more efficient since only one function will be called
|
|
||||||
self.func = func.func
|
|
||||||
self.args = func.args + args
|
|
||||||
self.keywords = {**func.keywords, **keywords}
|
|
||||||
else:
|
|
||||||
self.func = func
|
|
||||||
self.args = args
|
|
||||||
self.keywords = keywords
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
cls = type(self)
|
|
||||||
module = cls.__module__
|
|
||||||
qualname = cls.__qualname__
|
|
||||||
args = [repr(self.func)]
|
|
||||||
args.extend(map(repr, self.args))
|
|
||||||
args.extend(f"{k}={v!r}" for k, v in self.keywords.items())
|
|
||||||
return f"{module}.{qualname}({', '.join(args)})"
|
|
||||||
|
|
||||||
def _make_unbound_method(self):
|
def _make_unbound_method(self):
|
||||||
def _method(cls_or_self, /, *args, **keywords):
|
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}
|
keywords = {**self.keywords, **keywords}
|
||||||
return self.func(cls_or_self, *self.args, *args, **keywords)
|
return self.func(cls_or_self, *pto_args, *args, **keywords)
|
||||||
_method.__isabstractmethod__ = self.__isabstractmethod__
|
_method.__isabstractmethod__ = self.__isabstractmethod__
|
||||||
_method.__partialmethod__ = self
|
_method.__partialmethod__ = self
|
||||||
return _method
|
return _method
|
||||||
|
|
|
@ -1930,6 +1930,11 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()):
|
||||||
if param.kind is _POSITIONAL_ONLY:
|
if param.kind is _POSITIONAL_ONLY:
|
||||||
# If positional-only parameter is bound by partial,
|
# If positional-only parameter is bound by partial,
|
||||||
# it effectively disappears from the signature
|
# it effectively disappears from the signature
|
||||||
|
# However, if it is a Placeholder it is not removed
|
||||||
|
# And also looses default value
|
||||||
|
if arg_value is functools.Placeholder:
|
||||||
|
new_params[param_name] = param.replace(default=_empty)
|
||||||
|
else:
|
||||||
new_params.pop(param_name)
|
new_params.pop(param_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -1952,7 +1957,17 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()):
|
||||||
new_params[param_name] = param.replace(default=arg_value)
|
new_params[param_name] = param.replace(default=arg_value)
|
||||||
else:
|
else:
|
||||||
# was passed as a positional argument
|
# was passed as a positional argument
|
||||||
new_params.pop(param.name)
|
# Do not pop if it is a Placeholder
|
||||||
|
# also change kind to positional only
|
||||||
|
# and remove default
|
||||||
|
if arg_value is functools.Placeholder:
|
||||||
|
new_param = param.replace(
|
||||||
|
kind=_POSITIONAL_ONLY,
|
||||||
|
default=_empty
|
||||||
|
)
|
||||||
|
new_params[param_name] = new_param
|
||||||
|
else:
|
||||||
|
new_params.pop(param_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if param.kind is _KEYWORD_ONLY:
|
if param.kind is _KEYWORD_ONLY:
|
||||||
|
@ -2446,6 +2461,11 @@ def _signature_from_callable(obj, *,
|
||||||
sig_params = tuple(sig.parameters.values())
|
sig_params = tuple(sig.parameters.values())
|
||||||
assert (not sig_params or
|
assert (not sig_params or
|
||||||
first_wrapped_param is not sig_params[0])
|
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
|
new_params = (first_wrapped_param,) + sig_params
|
||||||
return sig.replace(parameters=new_params)
|
return sig.replace(parameters=new_params)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import copy
|
||||||
from itertools import permutations
|
from itertools import permutations
|
||||||
import pickle
|
import pickle
|
||||||
from random import choice
|
from random import choice
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from test import support
|
from test import support
|
||||||
import threading
|
import threading
|
||||||
|
@ -210,6 +211,51 @@ class TestPartial:
|
||||||
p2.new_attr = 'spam'
|
p2.new_attr = 'spam'
|
||||||
self.assertEqual(p2.new_attr, 'spam')
|
self.assertEqual(p2.new_attr, 'spam')
|
||||||
|
|
||||||
|
def test_placeholders_trailing_raise(self):
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
for args in [(PH,), (0, PH), (0, PH, 1, PH, PH, PH)]:
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.partial(capture, *args)
|
||||||
|
|
||||||
|
def test_placeholders(self):
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
# 1 Placeholder
|
||||||
|
args = (PH, 0)
|
||||||
|
p = self.partial(capture, *args)
|
||||||
|
actual_args, actual_kwds = p('x')
|
||||||
|
self.assertEqual(actual_args, ('x', 0))
|
||||||
|
self.assertEqual(actual_kwds, {})
|
||||||
|
# 2 Placeholders
|
||||||
|
args = (PH, 0, PH, 1)
|
||||||
|
p = self.partial(capture, *args)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
p('x')
|
||||||
|
actual_args, actual_kwds = p('x', 'y')
|
||||||
|
self.assertEqual(actual_args, ('x', 0, 'y', 1))
|
||||||
|
self.assertEqual(actual_kwds, {})
|
||||||
|
|
||||||
|
def test_placeholders_optimization(self):
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
p = self.partial(capture, PH, 0)
|
||||||
|
p2 = self.partial(p, PH, 1, 2, 3)
|
||||||
|
self.assertEqual(p2.args, (PH, 0, 1, 2, 3))
|
||||||
|
p3 = self.partial(p2, -1, 4)
|
||||||
|
actual_args, actual_kwds = p3(5)
|
||||||
|
self.assertEqual(actual_args, (-1, 0, 1, 2, 3, 4, 5))
|
||||||
|
self.assertEqual(actual_kwds, {})
|
||||||
|
# inner partial has placeholders and outer partial has no args case
|
||||||
|
p = self.partial(capture, PH, 0)
|
||||||
|
p2 = self.partial(p)
|
||||||
|
self.assertEqual(p2.args, (PH, 0))
|
||||||
|
self.assertEqual(p2(1), ((1, 0), {}))
|
||||||
|
|
||||||
|
def test_construct_placeholder_singleton(self):
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
tp = type(PH)
|
||||||
|
self.assertIs(tp(), PH)
|
||||||
|
self.assertRaises(TypeError, tp, 1, 2)
|
||||||
|
self.assertRaises(TypeError, tp, a=1, b=2)
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
args = (object(), object())
|
args = (object(), object())
|
||||||
args_repr = ', '.join(repr(a) for a in args)
|
args_repr = ', '.join(repr(a) for a in args)
|
||||||
|
@ -311,6 +357,23 @@ class TestPartial:
|
||||||
self.assertEqual(f(2), ((2,), {}))
|
self.assertEqual(f(2), ((2,), {}))
|
||||||
self.assertEqual(f(), ((), {}))
|
self.assertEqual(f(), ((), {}))
|
||||||
|
|
||||||
|
# Set State with placeholders
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
f = self.partial(signature)
|
||||||
|
f.__setstate__((capture, (PH, 1), dict(a=10), dict(attr=[])))
|
||||||
|
self.assertEqual(signature(f), (capture, (PH, 1), dict(a=10), dict(attr=[])))
|
||||||
|
msg_regex = re.escape("missing positional arguments in 'partial' call; "
|
||||||
|
"expected at least 1, got 0")
|
||||||
|
with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm:
|
||||||
|
f()
|
||||||
|
self.assertEqual(f(2), ((2, 1), dict(a=10)))
|
||||||
|
|
||||||
|
# Trailing Placeholder error
|
||||||
|
f = self.partial(signature)
|
||||||
|
msg_regex = re.escape("trailing Placeholders are not allowed")
|
||||||
|
with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm:
|
||||||
|
f.__setstate__((capture, (1, PH), dict(a=10), dict(attr=[])))
|
||||||
|
|
||||||
def test_setstate_errors(self):
|
def test_setstate_errors(self):
|
||||||
f = self.partial(signature)
|
f = self.partial(signature)
|
||||||
self.assertRaises(TypeError, f.__setstate__, (capture, (), {}))
|
self.assertRaises(TypeError, f.__setstate__, (capture, (), {}))
|
||||||
|
@ -456,6 +519,19 @@ class TestPartialC(TestPartial, unittest.TestCase):
|
||||||
self.assertIn('astr', r)
|
self.assertIn('astr', r)
|
||||||
self.assertIn("['sth']", r)
|
self.assertIn("['sth']", r)
|
||||||
|
|
||||||
|
def test_placeholders_refcount_smoke(self):
|
||||||
|
PH = self.module.Placeholder
|
||||||
|
# sum supports vector call
|
||||||
|
lst1, start = [], []
|
||||||
|
sum_lists = self.partial(sum, PH, start)
|
||||||
|
for i in range(10):
|
||||||
|
sum_lists([lst1, lst1])
|
||||||
|
# collections.ChainMap initializer does not support vectorcall
|
||||||
|
map1, map2 = {}, {}
|
||||||
|
partial_cm = self.partial(collections.ChainMap, PH, map1)
|
||||||
|
for i in range(10):
|
||||||
|
partial_cm(map2, map2)
|
||||||
|
|
||||||
|
|
||||||
class TestPartialPy(TestPartial, unittest.TestCase):
|
class TestPartialPy(TestPartial, unittest.TestCase):
|
||||||
module = py_functools
|
module = py_functools
|
||||||
|
@ -480,6 +556,19 @@ class TestPartialCSubclass(TestPartialC):
|
||||||
class TestPartialPySubclass(TestPartialPy):
|
class TestPartialPySubclass(TestPartialPy):
|
||||||
partial = PyPartialSubclass
|
partial = PyPartialSubclass
|
||||||
|
|
||||||
|
def test_subclass_optimization(self):
|
||||||
|
# `partial` input to `partial` subclass
|
||||||
|
p = py_functools.partial(min, 2)
|
||||||
|
p2 = self.partial(p, 1)
|
||||||
|
self.assertIs(p2.func, min)
|
||||||
|
self.assertEqual(p2(0), 0)
|
||||||
|
# `partial` subclass input to `partial` subclass
|
||||||
|
p = self.partial(min, 2)
|
||||||
|
p2 = self.partial(p, 1)
|
||||||
|
self.assertIs(p2.func, min)
|
||||||
|
self.assertEqual(p2(0), 0)
|
||||||
|
|
||||||
|
|
||||||
class TestPartialMethod(unittest.TestCase):
|
class TestPartialMethod(unittest.TestCase):
|
||||||
|
|
||||||
class A(object):
|
class A(object):
|
||||||
|
@ -617,6 +706,20 @@ class TestPartialMethod(unittest.TestCase):
|
||||||
p = functools.partial(f, 1)
|
p = functools.partial(f, 1)
|
||||||
self.assertEqual(p(2), f(1, 2))
|
self.assertEqual(p(2), f(1, 2))
|
||||||
|
|
||||||
|
def test_subclass_optimization(self):
|
||||||
|
class PartialMethodSubclass(functools.partialmethod):
|
||||||
|
pass
|
||||||
|
# `partialmethod` input to `partialmethod` subclass
|
||||||
|
p = functools.partialmethod(min, 2)
|
||||||
|
p2 = PartialMethodSubclass(p, 1)
|
||||||
|
self.assertIs(p2.func, min)
|
||||||
|
self.assertEqual(p2.__get__(0)(), 0)
|
||||||
|
# `partialmethod` subclass input to `partialmethod` subclass
|
||||||
|
p = PartialMethodSubclass(min, 2)
|
||||||
|
p2 = PartialMethodSubclass(p, 1)
|
||||||
|
self.assertIs(p2.func, min)
|
||||||
|
self.assertEqual(p2.__get__(0)(), 0)
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateWrapper(unittest.TestCase):
|
class TestUpdateWrapper(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
@ -3341,7 +3341,7 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
...))
|
...))
|
||||||
|
|
||||||
def test_signature_on_partial(self):
|
def test_signature_on_partial(self):
|
||||||
from functools import partial
|
from functools import partial, Placeholder
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
pass
|
pass
|
||||||
|
@ -3396,6 +3396,25 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
('d', ..., ..., "keyword_only")),
|
('d', ..., ..., "keyword_only")),
|
||||||
...))
|
...))
|
||||||
|
|
||||||
|
# With Placeholder
|
||||||
|
self.assertEqual(self.signature(partial(test, Placeholder, 1)),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('c', ..., ..., "keyword_only"),
|
||||||
|
('d', ..., ..., "keyword_only")),
|
||||||
|
...))
|
||||||
|
|
||||||
|
self.assertEqual(self.signature(partial(test, Placeholder, 1, c=2)),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('c', 2, ..., "keyword_only"),
|
||||||
|
('d', ..., ..., "keyword_only")),
|
||||||
|
...))
|
||||||
|
|
||||||
|
# Ensure unittest.mock.ANY & similar do not get picked up as a Placeholder
|
||||||
|
self.assertEqual(self.signature(partial(test, unittest.mock.ANY, 1, c=2)),
|
||||||
|
((('c', 2, ..., "keyword_only"),
|
||||||
|
('d', ..., ..., "keyword_only")),
|
||||||
|
...))
|
||||||
|
|
||||||
def test(a, *args, b, **kwargs):
|
def test(a, *args, b, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -3443,6 +3462,15 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
('kwargs', ..., ..., "var_keyword")),
|
('kwargs', ..., ..., "var_keyword")),
|
||||||
...))
|
...))
|
||||||
|
|
||||||
|
# With Placeholder
|
||||||
|
p = partial(test, Placeholder, Placeholder, 1, b=0, test=1)
|
||||||
|
self.assertEqual(self.signature(p),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('args', ..., ..., "var_positional"),
|
||||||
|
('b', 0, ..., "keyword_only"),
|
||||||
|
('kwargs', ..., ..., "var_keyword")),
|
||||||
|
...))
|
||||||
|
|
||||||
def test(a, b, c:int) -> 42:
|
def test(a, b, c:int) -> 42:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -3547,6 +3575,34 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
('kwargs', ..., ..., 'var_keyword')),
|
('kwargs', ..., ..., 'var_keyword')),
|
||||||
...))
|
...))
|
||||||
|
|
||||||
|
# Positional only With Placeholder
|
||||||
|
p = partial(foo, Placeholder, 1, c=0, d=1)
|
||||||
|
self.assertEqual(self.signature(p),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('c', 0, ..., "keyword_only"),
|
||||||
|
('d', 1, ..., "keyword_only"),
|
||||||
|
('kwargs', ..., ..., "var_keyword")),
|
||||||
|
...))
|
||||||
|
|
||||||
|
# Optionals Positional With Placeholder
|
||||||
|
def foo(a=0, b=1, /, c=2, d=3):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Positional
|
||||||
|
p = partial(foo, Placeholder, 1, c=0, d=1)
|
||||||
|
self.assertEqual(self.signature(p),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('c', 0, ..., "keyword_only"),
|
||||||
|
('d', 1, ..., "keyword_only")),
|
||||||
|
...))
|
||||||
|
|
||||||
|
# Positional or Keyword - transformed to positional
|
||||||
|
p = partial(foo, Placeholder, 1, Placeholder, 1)
|
||||||
|
self.assertEqual(self.signature(p),
|
||||||
|
((('a', ..., ..., "positional_only"),
|
||||||
|
('c', ..., ..., "positional_only")),
|
||||||
|
...))
|
||||||
|
|
||||||
def test_signature_on_partialmethod(self):
|
def test_signature_on_partialmethod(self):
|
||||||
from functools import partialmethod
|
from functools import partialmethod
|
||||||
|
|
||||||
|
@ -3559,18 +3615,32 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
inspect.signature(Spam.ham)
|
inspect.signature(Spam.ham)
|
||||||
|
|
||||||
class Spam:
|
class Spam:
|
||||||
def test(it, a, *, c) -> 'spam':
|
def test(it, a, b, *, c) -> 'spam':
|
||||||
pass
|
pass
|
||||||
ham = partialmethod(test, c=1)
|
ham = partialmethod(test, c=1)
|
||||||
|
bar = partialmethod(test, functools.Placeholder, 1, c=1)
|
||||||
|
|
||||||
self.assertEqual(self.signature(Spam.ham, eval_str=False),
|
self.assertEqual(self.signature(Spam.ham, eval_str=False),
|
||||||
((('it', ..., ..., 'positional_or_keyword'),
|
((('it', ..., ..., 'positional_or_keyword'),
|
||||||
('a', ..., ..., 'positional_or_keyword'),
|
('a', ..., ..., 'positional_or_keyword'),
|
||||||
|
('b', ..., ..., 'positional_or_keyword'),
|
||||||
('c', 1, ..., 'keyword_only')),
|
('c', 1, ..., 'keyword_only')),
|
||||||
'spam'))
|
'spam'))
|
||||||
|
|
||||||
self.assertEqual(self.signature(Spam().ham, eval_str=False),
|
self.assertEqual(self.signature(Spam().ham, eval_str=False),
|
||||||
((('a', ..., ..., 'positional_or_keyword'),
|
((('a', ..., ..., 'positional_or_keyword'),
|
||||||
|
('b', ..., ..., 'positional_or_keyword'),
|
||||||
|
('c', 1, ..., 'keyword_only')),
|
||||||
|
'spam'))
|
||||||
|
|
||||||
|
# With Placeholder
|
||||||
|
self.assertEqual(self.signature(Spam.bar, eval_str=False),
|
||||||
|
((('it', ..., ..., 'positional_only'),
|
||||||
|
('a', ..., ..., 'positional_only'),
|
||||||
|
('c', 1, ..., 'keyword_only')),
|
||||||
|
'spam'))
|
||||||
|
self.assertEqual(self.signature(Spam().bar, eval_str=False),
|
||||||
|
((('a', ..., ..., 'positional_only'),
|
||||||
('c', 1, ..., 'keyword_only')),
|
('c', 1, ..., 'keyword_only')),
|
||||||
'spam'))
|
'spam'))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Positional arguments of :func:`functools.partial` objects
|
||||||
|
now support placeholders via :data:`functools.Placeholder`.
|
|
@ -25,6 +25,8 @@ class _functools._lru_cache_wrapper "PyObject *" "&lru_cache_type_spec"
|
||||||
typedef struct _functools_state {
|
typedef struct _functools_state {
|
||||||
/* this object is used delimit args and keywords in the cache keys */
|
/* this object is used delimit args and keywords in the cache keys */
|
||||||
PyObject *kwd_mark;
|
PyObject *kwd_mark;
|
||||||
|
PyTypeObject *placeholder_type;
|
||||||
|
PyObject *placeholder;
|
||||||
PyTypeObject *partial_type;
|
PyTypeObject *partial_type;
|
||||||
PyTypeObject *keyobject_type;
|
PyTypeObject *keyobject_type;
|
||||||
PyTypeObject *lru_list_elem_type;
|
PyTypeObject *lru_list_elem_type;
|
||||||
|
@ -41,6 +43,79 @@ get_functools_state(PyObject *module)
|
||||||
|
|
||||||
/* partial object **********************************************************/
|
/* partial object **********************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
// The 'Placeholder' singleton indicates which formal positional
|
||||||
|
// parameters are to be bound first when using a 'partial' object.
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
} placeholderobject;
|
||||||
|
|
||||||
|
static inline _functools_state *
|
||||||
|
get_functools_state_by_type(PyTypeObject *type);
|
||||||
|
|
||||||
|
PyDoc_STRVAR(placeholder_doc,
|
||||||
|
"The type of the Placeholder singleton.\n\n"
|
||||||
|
"Used as a placeholder for partial arguments.");
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
placeholder_repr(PyObject *op)
|
||||||
|
{
|
||||||
|
return PyUnicode_FromString("Placeholder");
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
placeholder_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
return PyUnicode_FromString("Placeholder");
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyMethodDef placeholder_methods[] = {
|
||||||
|
{"__reduce__", placeholder_reduce, METH_NOARGS, NULL},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
placeholder_dealloc(PyObject* placeholder)
|
||||||
|
{
|
||||||
|
/* This should never get called, but we also don't want to SEGV if
|
||||||
|
* we accidentally decref Placeholder out of existence. Instead,
|
||||||
|
* since Placeholder is an immortal object, re-set the reference count.
|
||||||
|
*/
|
||||||
|
_Py_SetImmortal(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
placeholder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
|
||||||
|
{
|
||||||
|
if (PyTuple_GET_SIZE(args) || (kwargs && PyDict_GET_SIZE(kwargs))) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "PlaceholderType takes no arguments");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
_functools_state *state = get_functools_state_by_type(type);
|
||||||
|
if (state->placeholder == NULL) {
|
||||||
|
state->placeholder = PyType_GenericNew(type, NULL, NULL);
|
||||||
|
}
|
||||||
|
return state->placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyType_Slot placeholder_type_slots[] = {
|
||||||
|
{Py_tp_dealloc, placeholder_dealloc},
|
||||||
|
{Py_tp_repr, placeholder_repr},
|
||||||
|
{Py_tp_doc, (void *)placeholder_doc},
|
||||||
|
{Py_tp_methods, placeholder_methods},
|
||||||
|
{Py_tp_new, placeholder_new},
|
||||||
|
{0, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PyType_Spec placeholder_type_spec = {
|
||||||
|
.name = "functools._PlaceholderType",
|
||||||
|
.basicsize = sizeof(placeholderobject),
|
||||||
|
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE,
|
||||||
|
.slots = placeholder_type_slots
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
PyObject *fn;
|
PyObject *fn;
|
||||||
|
@ -48,6 +123,8 @@ typedef struct {
|
||||||
PyObject *kw;
|
PyObject *kw;
|
||||||
PyObject *dict; /* __dict__ */
|
PyObject *dict; /* __dict__ */
|
||||||
PyObject *weakreflist; /* List of weak references */
|
PyObject *weakreflist; /* List of weak references */
|
||||||
|
PyObject *placeholder; /* Placeholder for positional arguments */
|
||||||
|
Py_ssize_t phcount; /* Number of placeholders */
|
||||||
vectorcallfunc vectorcall;
|
vectorcallfunc vectorcall;
|
||||||
} partialobject;
|
} partialobject;
|
||||||
|
|
||||||
|
@ -70,23 +147,38 @@ get_functools_state_by_type(PyTypeObject *type)
|
||||||
static PyObject *
|
static PyObject *
|
||||||
partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
||||||
{
|
{
|
||||||
PyObject *func, *pargs, *nargs, *pkw;
|
PyObject *func, *pto_args, *new_args, *pto_kw, *phold;
|
||||||
partialobject *pto;
|
partialobject *pto;
|
||||||
|
Py_ssize_t pto_phcount = 0;
|
||||||
|
Py_ssize_t new_nargs = PyTuple_GET_SIZE(args) - 1;
|
||||||
|
|
||||||
if (PyTuple_GET_SIZE(args) < 1) {
|
if (new_nargs < 0) {
|
||||||
PyErr_SetString(PyExc_TypeError,
|
PyErr_SetString(PyExc_TypeError,
|
||||||
"type 'partial' takes at least one argument");
|
"type 'partial' takes at least one argument");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
func = PyTuple_GET_ITEM(args, 0);
|
||||||
|
if (!PyCallable_Check(func)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"the first argument must be callable");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
_functools_state *state = get_functools_state_by_type(type);
|
_functools_state *state = get_functools_state_by_type(type);
|
||||||
if (state == NULL) {
|
if (state == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
phold = state->placeholder;
|
||||||
|
|
||||||
pargs = pkw = NULL;
|
/* Placeholder restrictions */
|
||||||
func = PyTuple_GET_ITEM(args, 0);
|
if (new_nargs && PyTuple_GET_ITEM(args, new_nargs) == phold) {
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"trailing Placeholders are not allowed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check wrapped function / object */
|
||||||
|
pto_args = pto_kw = NULL;
|
||||||
int res = PyObject_TypeCheck(func, state->partial_type);
|
int res = PyObject_TypeCheck(func, state->partial_type);
|
||||||
if (res == -1) {
|
if (res == -1) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -95,18 +187,14 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
||||||
// We can use its underlying function directly and merge the arguments.
|
// We can use its underlying function directly and merge the arguments.
|
||||||
partialobject *part = (partialobject *)func;
|
partialobject *part = (partialobject *)func;
|
||||||
if (part->dict == NULL) {
|
if (part->dict == NULL) {
|
||||||
pargs = part->args;
|
pto_args = part->args;
|
||||||
pkw = part->kw;
|
pto_kw = part->kw;
|
||||||
func = part->fn;
|
func = part->fn;
|
||||||
assert(PyTuple_Check(pargs));
|
pto_phcount = part->phcount;
|
||||||
assert(PyDict_Check(pkw));
|
assert(PyTuple_Check(pto_args));
|
||||||
|
assert(PyDict_Check(pto_kw));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!PyCallable_Check(func)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError,
|
|
||||||
"the first argument must be callable");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* create partialobject structure */
|
/* create partialobject structure */
|
||||||
pto = (partialobject *)type->tp_alloc(type, 0);
|
pto = (partialobject *)type->tp_alloc(type, 0);
|
||||||
|
@ -114,18 +202,58 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
pto->fn = Py_NewRef(func);
|
pto->fn = Py_NewRef(func);
|
||||||
|
pto->placeholder = phold;
|
||||||
|
|
||||||
nargs = PyTuple_GetSlice(args, 1, PY_SSIZE_T_MAX);
|
new_args = PyTuple_GetSlice(args, 1, new_nargs + 1);
|
||||||
if (nargs == NULL) {
|
if (new_args == NULL) {
|
||||||
Py_DECREF(pto);
|
Py_DECREF(pto);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (pargs == NULL) {
|
|
||||||
pto->args = nargs;
|
/* Count placeholders */
|
||||||
|
Py_ssize_t phcount = 0;
|
||||||
|
for (Py_ssize_t i = 0; i < new_nargs - 1; i++) {
|
||||||
|
if (PyTuple_GET_ITEM(new_args, i) == phold) {
|
||||||
|
phcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* merge args with args of `func` which is `partial` */
|
||||||
|
if (pto_phcount > 0 && new_nargs > 0) {
|
||||||
|
Py_ssize_t npargs = PyTuple_GET_SIZE(pto_args);
|
||||||
|
Py_ssize_t tot_nargs = npargs;
|
||||||
|
if (new_nargs > pto_phcount) {
|
||||||
|
tot_nargs += new_nargs - pto_phcount;
|
||||||
|
}
|
||||||
|
PyObject *item;
|
||||||
|
PyObject *tot_args = PyTuple_New(tot_nargs);
|
||||||
|
for (Py_ssize_t i = 0, j = 0; i < tot_nargs; i++) {
|
||||||
|
if (i < npargs) {
|
||||||
|
item = PyTuple_GET_ITEM(pto_args, i);
|
||||||
|
if (j < new_nargs && item == phold) {
|
||||||
|
item = PyTuple_GET_ITEM(new_args, j);
|
||||||
|
j++;
|
||||||
|
pto_phcount--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pto->args = PySequence_Concat(pargs, nargs);
|
item = PyTuple_GET_ITEM(new_args, j);
|
||||||
Py_DECREF(nargs);
|
j++;
|
||||||
|
}
|
||||||
|
Py_INCREF(item);
|
||||||
|
PyTuple_SET_ITEM(tot_args, i, item);
|
||||||
|
}
|
||||||
|
pto->args = tot_args;
|
||||||
|
pto->phcount = pto_phcount + phcount;
|
||||||
|
Py_DECREF(new_args);
|
||||||
|
}
|
||||||
|
else if (pto_args == NULL) {
|
||||||
|
pto->args = new_args;
|
||||||
|
pto->phcount = phcount;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pto->args = PySequence_Concat(pto_args, new_args);
|
||||||
|
pto->phcount = pto_phcount + phcount;
|
||||||
|
Py_DECREF(new_args);
|
||||||
if (pto->args == NULL) {
|
if (pto->args == NULL) {
|
||||||
Py_DECREF(pto);
|
Py_DECREF(pto);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -133,7 +261,7 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
||||||
assert(PyTuple_Check(pto->args));
|
assert(PyTuple_Check(pto->args));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pkw == NULL || PyDict_GET_SIZE(pkw) == 0) {
|
if (pto_kw == NULL || PyDict_GET_SIZE(pto_kw) == 0) {
|
||||||
if (kw == NULL) {
|
if (kw == NULL) {
|
||||||
pto->kw = PyDict_New();
|
pto->kw = PyDict_New();
|
||||||
}
|
}
|
||||||
|
@ -145,7 +273,7 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pto->kw = PyDict_Copy(pkw);
|
pto->kw = PyDict_Copy(pto_kw);
|
||||||
if (kw != NULL && pto->kw != NULL) {
|
if (kw != NULL && pto->kw != NULL) {
|
||||||
if (PyDict_Merge(pto->kw, kw, 1) != 0) {
|
if (PyDict_Merge(pto->kw, kw, 1) != 0) {
|
||||||
Py_DECREF(pto);
|
Py_DECREF(pto);
|
||||||
|
@ -225,23 +353,30 @@ partial_vectorcall(partialobject *pto, PyObject *const *args,
|
||||||
size_t nargsf, PyObject *kwnames)
|
size_t nargsf, PyObject *kwnames)
|
||||||
{
|
{
|
||||||
PyThreadState *tstate = _PyThreadState_GET();
|
PyThreadState *tstate = _PyThreadState_GET();
|
||||||
|
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
|
||||||
|
|
||||||
/* pto->kw is mutable, so need to check every time */
|
/* pto->kw is mutable, so need to check every time */
|
||||||
if (PyDict_GET_SIZE(pto->kw)) {
|
if (PyDict_GET_SIZE(pto->kw)) {
|
||||||
return partial_vectorcall_fallback(tstate, pto, args, nargsf, kwnames);
|
return partial_vectorcall_fallback(tstate, pto, args, nargsf, kwnames);
|
||||||
}
|
}
|
||||||
|
Py_ssize_t pto_phcount = pto->phcount;
|
||||||
|
if (nargs < pto_phcount) {
|
||||||
|
PyErr_Format(PyExc_TypeError,
|
||||||
|
"missing positional arguments in 'partial' call; "
|
||||||
|
"expected at least %zd, got %zd", pto_phcount, nargs);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
|
Py_ssize_t nargskw = nargs;
|
||||||
Py_ssize_t nargs_total = nargs;
|
|
||||||
if (kwnames != NULL) {
|
if (kwnames != NULL) {
|
||||||
nargs_total += PyTuple_GET_SIZE(kwnames);
|
nargskw += PyTuple_GET_SIZE(kwnames);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject **pto_args = _PyTuple_ITEMS(pto->args);
|
PyObject **pto_args = _PyTuple_ITEMS(pto->args);
|
||||||
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(pto->args);
|
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(pto->args);
|
||||||
|
|
||||||
/* Fast path if we're called without arguments */
|
/* Fast path if we're called without arguments */
|
||||||
if (nargs_total == 0) {
|
if (nargskw == 0) {
|
||||||
return _PyObject_VectorcallTstate(tstate, pto->fn,
|
return _PyObject_VectorcallTstate(tstate, pto->fn,
|
||||||
pto_args, pto_nargs, NULL);
|
pto_args, pto_nargs, NULL);
|
||||||
}
|
}
|
||||||
|
@ -258,29 +393,47 @@ partial_vectorcall(partialobject *pto, PyObject *const *args,
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_ssize_t newnargs_total = pto_nargs + nargs_total;
|
|
||||||
|
|
||||||
PyObject *small_stack[_PY_FASTCALL_SMALL_STACK];
|
PyObject *small_stack[_PY_FASTCALL_SMALL_STACK];
|
||||||
PyObject *ret;
|
|
||||||
PyObject **stack;
|
PyObject **stack;
|
||||||
|
|
||||||
if (newnargs_total <= (Py_ssize_t)Py_ARRAY_LENGTH(small_stack)) {
|
Py_ssize_t tot_nargskw = pto_nargs + nargskw - pto_phcount;
|
||||||
|
if (tot_nargskw <= (Py_ssize_t)Py_ARRAY_LENGTH(small_stack)) {
|
||||||
stack = small_stack;
|
stack = small_stack;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
stack = PyMem_Malloc(newnargs_total * sizeof(PyObject *));
|
stack = PyMem_Malloc(tot_nargskw * sizeof(PyObject *));
|
||||||
if (stack == NULL) {
|
if (stack == NULL) {
|
||||||
PyErr_NoMemory();
|
PyErr_NoMemory();
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Py_ssize_t tot_nargs;
|
||||||
|
if (pto_phcount) {
|
||||||
|
tot_nargs = pto_nargs + nargs - pto_phcount;
|
||||||
|
Py_ssize_t j = 0; // New args index
|
||||||
|
for (Py_ssize_t i = 0; i < pto_nargs; i++) {
|
||||||
|
if (pto_args[i] == pto->placeholder) {
|
||||||
|
stack[i] = args[j];
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stack[i] = pto_args[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(j == pto_phcount);
|
||||||
|
if (nargskw > pto_phcount) {
|
||||||
|
memcpy(stack + pto_nargs, args + j, (nargskw - j) * sizeof(PyObject*));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tot_nargs = pto_nargs + nargs;
|
||||||
/* Copy to new stack, using borrowed references */
|
/* Copy to new stack, using borrowed references */
|
||||||
memcpy(stack, pto_args, pto_nargs * sizeof(PyObject*));
|
memcpy(stack, pto_args, pto_nargs * sizeof(PyObject*));
|
||||||
memcpy(stack + pto_nargs, args, nargs_total * sizeof(PyObject*));
|
memcpy(stack + pto_nargs, args, nargskw * sizeof(PyObject*));
|
||||||
|
}
|
||||||
ret = _PyObject_VectorcallTstate(tstate, pto->fn,
|
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn,
|
||||||
stack, pto_nargs + nargs, kwnames);
|
stack, tot_nargs, kwnames);
|
||||||
if (stack != small_stack) {
|
if (stack != small_stack) {
|
||||||
PyMem_Free(stack);
|
PyMem_Free(stack);
|
||||||
}
|
}
|
||||||
|
@ -312,40 +465,81 @@ partial_call(partialobject *pto, PyObject *args, PyObject *kwargs)
|
||||||
assert(PyTuple_Check(pto->args));
|
assert(PyTuple_Check(pto->args));
|
||||||
assert(PyDict_Check(pto->kw));
|
assert(PyDict_Check(pto->kw));
|
||||||
|
|
||||||
|
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
|
||||||
|
Py_ssize_t pto_phcount = pto->phcount;
|
||||||
|
if (nargs < pto_phcount) {
|
||||||
|
PyErr_Format(PyExc_TypeError,
|
||||||
|
"missing positional arguments in 'partial' call; "
|
||||||
|
"expected at least %zd, got %zd", pto_phcount, nargs);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Merge keywords */
|
/* Merge keywords */
|
||||||
PyObject *kwargs2;
|
PyObject *tot_kw;
|
||||||
if (PyDict_GET_SIZE(pto->kw) == 0) {
|
if (PyDict_GET_SIZE(pto->kw) == 0) {
|
||||||
/* kwargs can be NULL */
|
/* kwargs can be NULL */
|
||||||
kwargs2 = Py_XNewRef(kwargs);
|
tot_kw = Py_XNewRef(kwargs);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
/* bpo-27840, bpo-29318: dictionary of keyword parameters must be
|
/* bpo-27840, bpo-29318: dictionary of keyword parameters must be
|
||||||
copied, because a function using "**kwargs" can modify the
|
copied, because a function using "**kwargs" can modify the
|
||||||
dictionary. */
|
dictionary. */
|
||||||
kwargs2 = PyDict_Copy(pto->kw);
|
tot_kw = PyDict_Copy(pto->kw);
|
||||||
if (kwargs2 == NULL) {
|
if (tot_kw == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kwargs != NULL) {
|
if (kwargs != NULL) {
|
||||||
if (PyDict_Merge(kwargs2, kwargs, 1) != 0) {
|
if (PyDict_Merge(tot_kw, kwargs, 1) != 0) {
|
||||||
Py_DECREF(kwargs2);
|
Py_DECREF(tot_kw);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Merge positional arguments */
|
/* Merge positional arguments */
|
||||||
/* Note: tupleconcat() is optimized for empty tuples */
|
PyObject *tot_args;
|
||||||
PyObject *args2 = PySequence_Concat(pto->args, args);
|
if (pto_phcount) {
|
||||||
if (args2 == NULL) {
|
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(pto->args);
|
||||||
Py_XDECREF(kwargs2);
|
Py_ssize_t tot_nargs = pto_nargs + nargs - pto_phcount;
|
||||||
|
assert(tot_nargs >= 0);
|
||||||
|
tot_args = PyTuple_New(tot_nargs);
|
||||||
|
if (tot_args == NULL) {
|
||||||
|
Py_XDECREF(tot_kw);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
PyObject *pto_args = pto->args;
|
||||||
|
PyObject *item;
|
||||||
|
Py_ssize_t j = 0; // New args index
|
||||||
|
for (Py_ssize_t i = 0; i < pto_nargs; i++) {
|
||||||
|
item = PyTuple_GET_ITEM(pto_args, i);
|
||||||
|
if (item == pto->placeholder) {
|
||||||
|
item = PyTuple_GET_ITEM(args, j);
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
Py_INCREF(item);
|
||||||
|
PyTuple_SET_ITEM(tot_args, i, item);
|
||||||
|
}
|
||||||
|
assert(j == pto_phcount);
|
||||||
|
for (Py_ssize_t i = pto_nargs; i < tot_nargs; i++) {
|
||||||
|
item = PyTuple_GET_ITEM(args, j);
|
||||||
|
Py_INCREF(item);
|
||||||
|
PyTuple_SET_ITEM(tot_args, i, item);
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Note: tupleconcat() is optimized for empty tuples */
|
||||||
|
tot_args = PySequence_Concat(pto->args, args);
|
||||||
|
if (tot_args == NULL) {
|
||||||
|
Py_XDECREF(tot_kw);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PyObject *res = PyObject_Call(pto->fn, args2, kwargs2);
|
PyObject *res = PyObject_Call(pto->fn, tot_args, tot_kw);
|
||||||
Py_DECREF(args2);
|
Py_DECREF(tot_args);
|
||||||
Py_XDECREF(kwargs2);
|
Py_XDECREF(tot_kw);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,8 +655,11 @@ partial_setstate(partialobject *pto, PyObject *state)
|
||||||
{
|
{
|
||||||
PyObject *fn, *fnargs, *kw, *dict;
|
PyObject *fn, *fnargs, *kw, *dict;
|
||||||
|
|
||||||
if (!PyTuple_Check(state) ||
|
if (!PyTuple_Check(state)) {
|
||||||
!PyArg_ParseTuple(state, "OOOO", &fn, &fnargs, &kw, &dict) ||
|
PyErr_SetString(PyExc_TypeError, "invalid partial state");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!PyArg_ParseTuple(state, "OOOO", &fn, &fnargs, &kw, &dict) ||
|
||||||
!PyCallable_Check(fn) ||
|
!PyCallable_Check(fn) ||
|
||||||
!PyTuple_Check(fnargs) ||
|
!PyTuple_Check(fnargs) ||
|
||||||
(kw != Py_None && !PyDict_Check(kw)))
|
(kw != Py_None && !PyDict_Check(kw)))
|
||||||
|
@ -471,6 +668,20 @@ partial_setstate(partialobject *pto, PyObject *state)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Py_ssize_t nargs = PyTuple_GET_SIZE(fnargs);
|
||||||
|
if (nargs && PyTuple_GET_ITEM(fnargs, nargs - 1) == pto->placeholder) {
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"trailing Placeholders are not allowed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* Count placeholders */
|
||||||
|
Py_ssize_t phcount = 0;
|
||||||
|
for (Py_ssize_t i = 0; i < nargs - 1; i++) {
|
||||||
|
if (PyTuple_GET_ITEM(fnargs, i) == pto->placeholder) {
|
||||||
|
phcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!PyTuple_CheckExact(fnargs))
|
if(!PyTuple_CheckExact(fnargs))
|
||||||
fnargs = PySequence_Tuple(fnargs);
|
fnargs = PySequence_Tuple(fnargs);
|
||||||
else
|
else
|
||||||
|
@ -493,10 +704,10 @@ partial_setstate(partialobject *pto, PyObject *state)
|
||||||
dict = NULL;
|
dict = NULL;
|
||||||
else
|
else
|
||||||
Py_INCREF(dict);
|
Py_INCREF(dict);
|
||||||
|
|
||||||
Py_SETREF(pto->fn, Py_NewRef(fn));
|
Py_SETREF(pto->fn, Py_NewRef(fn));
|
||||||
Py_SETREF(pto->args, fnargs);
|
Py_SETREF(pto->args, fnargs);
|
||||||
Py_SETREF(pto->kw, kw);
|
Py_SETREF(pto->kw, kw);
|
||||||
|
pto->phcount = phcount;
|
||||||
Py_XSETREF(pto->dict, dict);
|
Py_XSETREF(pto->dict, dict);
|
||||||
partial_setvectorcall(pto);
|
partial_setvectorcall(pto);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
@ -1498,6 +1709,21 @@ _functools_exec(PyObject *module)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state->placeholder_type = (PyTypeObject *)PyType_FromModuleAndSpec(module,
|
||||||
|
&placeholder_type_spec, NULL);
|
||||||
|
if (state->placeholder_type == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (PyModule_AddType(module, state->placeholder_type) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
state->placeholder = PyObject_CallNoArgs((PyObject *)state->placeholder_type);
|
||||||
|
if (state->placeholder == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (PyModule_AddObject(module, "Placeholder", state->placeholder) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
state->partial_type = (PyTypeObject *)PyType_FromModuleAndSpec(module,
|
state->partial_type = (PyTypeObject *)PyType_FromModuleAndSpec(module,
|
||||||
&partial_type_spec, NULL);
|
&partial_type_spec, NULL);
|
||||||
if (state->partial_type == NULL) {
|
if (state->partial_type == NULL) {
|
||||||
|
@ -1542,6 +1768,7 @@ _functools_traverse(PyObject *module, visitproc visit, void *arg)
|
||||||
{
|
{
|
||||||
_functools_state *state = get_functools_state(module);
|
_functools_state *state = get_functools_state(module);
|
||||||
Py_VISIT(state->kwd_mark);
|
Py_VISIT(state->kwd_mark);
|
||||||
|
Py_VISIT(state->placeholder_type);
|
||||||
Py_VISIT(state->partial_type);
|
Py_VISIT(state->partial_type);
|
||||||
Py_VISIT(state->keyobject_type);
|
Py_VISIT(state->keyobject_type);
|
||||||
Py_VISIT(state->lru_list_elem_type);
|
Py_VISIT(state->lru_list_elem_type);
|
||||||
|
@ -1553,6 +1780,7 @@ _functools_clear(PyObject *module)
|
||||||
{
|
{
|
||||||
_functools_state *state = get_functools_state(module);
|
_functools_state *state = get_functools_state(module);
|
||||||
Py_CLEAR(state->kwd_mark);
|
Py_CLEAR(state->kwd_mark);
|
||||||
|
Py_CLEAR(state->placeholder_type);
|
||||||
Py_CLEAR(state->partial_type);
|
Py_CLEAR(state->partial_type);
|
||||||
Py_CLEAR(state->keyobject_type);
|
Py_CLEAR(state->keyobject_type);
|
||||||
Py_CLEAR(state->lru_list_elem_type);
|
Py_CLEAR(state->lru_list_elem_type);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue