Skip to content

Commit 90dc0fd

Browse files
committed
trailing placeholder restriction removed
1 parent f43b69e commit 90dc0fd

4 files changed

Lines changed: 59 additions & 66 deletions

File tree

Lib/functools.py

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,6 @@ class partial:
334334
def __new__(cls, func, /, *args, **keywords):
335335
if not callable(func):
336336
raise TypeError("the first argument must be callable")
337-
if args and args[-1] is Placeholder:
338-
# Trim trailing placeholders
339-
j = len(args) - 1
340-
if not j:
341-
args = ()
342-
else:
343-
while (j := j - 1) >= 0:
344-
if args[j] is not Placeholder:
345-
break
346-
args = args[:j + 1]
347337
if isinstance(func, partial):
348338
pto_phcount = func._phcount
349339
tot_args = func.args
@@ -411,8 +401,8 @@ def __setstate__(self, state):
411401
(namespace is not None and not isinstance(namespace, dict))):
412402
raise TypeError("invalid partial state")
413403

414-
if args and args[-1] is Placeholder:
415-
raise TypeError("unexpected trailing Placeholders")
404+
# if args and args[-1] is Placeholder:
405+
# raise TypeError("unexpected trailing Placeholders")
416406
phcount, merger = _partial_prepare_merger(args)
417407

418408
args = tuple(args) # just in case it's a subclass
@@ -435,6 +425,7 @@ def __setstate__(self, state):
435425
except ImportError:
436426
pass
437427

428+
438429
# Descriptor version
439430
class partialmethod:
440431
"""Method descriptor with partial application of the given arguments
@@ -446,50 +437,49 @@ class partialmethod:
446437

447438
__slots__ = ("func", "args", "keywords", "wrapper",
448439
"__isabstractmethod__", "__dict__", "__weakref__")
440+
449441
__repr__ = _partial_repr
442+
__class_getitem__ = classmethod(GenericAlias)
450443

451444
def __init__(self, func, /, *args, **keywords):
452445
if isinstance(func, partialmethod):
453446
# Subclass optimization
454447
temp = partial(lambda: None, *func.args, **func.keywords)
455448
temp = partial(temp, *args, **keywords)
449+
isabstract = func.__isabstractmethod__
456450
func = func.func
457451
args = temp.args
458452
keywords = temp.keywords
453+
else:
454+
isabstract = getattr(func, '__isabstractmethod__', False)
459455
self.func = func
460456
self.args = args
461457
self.keywords = keywords
462-
self.__isabstractmethod__ = getattr(func, "__isabstractmethod__", False)
458+
self.__isabstractmethod__ = isabstract
463459

464460
# 5 cases
465-
rewrap = None
466461
if isinstance(func, staticmethod):
467-
self.wrapper = partial(func.__wrapped__, *args, **keywords)
468-
rewrap = staticmethod
462+
wrapper = partial(func.__wrapped__, *args, **keywords)
463+
self.wrapper = _rewrap_func(wrapper, isabstract, staticmethod)
469464
elif isinstance(func, classmethod):
470-
self.wrapper = partial(func.__wrapped__, Placeholder, *args, **keywords)
471-
rewrap = classmethod
465+
wrapper = _partial_unbound(func.__wrapped__, args, keywords)
466+
self.wrapper = _rewrap_func(wrapper, isabstract, classmethod)
472467
elif isinstance(func, (FunctionType, partial)):
473468
# instance method
474-
self.wrapper = partial(func, Placeholder, *args, **keywords)
469+
wrapper = _partial_unbound(func, args, keywords)
470+
self.wrapper = _rewrap_func(wrapper, isabstract)
475471
elif getattr(func, '__get__', None) is None:
476-
if not callable(func):
477-
raise TypeError(f"the first argument {func!r} must be a callable "
478-
"or a descriptor")
479472
# callable object without __get__
480473
# treat this like an instance method
481-
self.wrapper = partial(func, Placeholder, *args, **keywords)
474+
if not callable(func):
475+
raise TypeError(f'the first argument {func!r} must be a callable '
476+
'or a descriptor')
477+
wrapper = _partial_unbound(func, args, keywords)
478+
self.wrapper = _rewrap_func(wrapper, isabstract)
482479
else:
483480
# Unknown descriptor
484481
self.wrapper = None
485482

486-
# Adjust for abstract and rewrap if needed
487-
if self.wrapper is not None:
488-
if self.__isabstractmethod__:
489-
self.wrapper = abstractmethod(self.wrapper)
490-
if rewrap is not None:
491-
self.wrapper = rewrap(self.wrapper)
492-
493483
def __get__(self, obj, cls=None):
494484
if self.wrapper is not None:
495485
return self.wrapper.__get__(obj, cls)
@@ -503,25 +493,32 @@ def __get__(self, obj, cls=None):
503493
pass
504494
return result
505495

506-
__class_getitem__ = classmethod(GenericAlias)
507-
508496

509497
# Helper functions
510498

499+
def _partial_unbound(func, args, keywords):
500+
if not args:
501+
return partial(func, **keywords)
502+
return partial(func, Placeholder, *args, **keywords)
503+
504+
def _rewrap_func(func, isabstract, decorator=None):
505+
if isabstract:
506+
func = abstractmethod(func)
507+
if decorator is not None:
508+
func = decorator(func)
509+
return func
510+
511511
def _unwrap_partial(func):
512512
while isinstance(func, partial):
513513
func = func.func
514514
return func
515515

516516
def _unwrap_partialmethod(func):
517-
prev = None
518-
while func is not prev:
519-
prev = func
520-
while isinstance(func, partialmethod):
521-
func = getattr(func, 'func')
522-
func = _unwrap_partial(func)
517+
while isinstance(func, (partial, partialmethod)):
518+
func = func.func
523519
return func
524520

521+
525522
################################################################################
526523
### LRU Cache function decorator
527524
################################################################################

Lib/test/test_functools.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,16 @@ def foo(bar):
211211
p2.new_attr = 'spam'
212212
self.assertEqual(p2.new_attr, 'spam')
213213

214-
def test_placeholders_trailing_trim(self):
215-
PH = self.module.Placeholder
216-
for args, call_args, expected_args in [
217-
[(PH,), (), ()],
218-
[(0, PH), (), (0,)],
219-
[(0, PH, 1, PH, PH, PH), (2,), (0, 2, 1)]
220-
]:
221-
actual_args, actual_kwds = self.partial(capture, *args)(*call_args)
222-
self.assertEqual(actual_args, expected_args)
223-
self.assertEqual(actual_kwds, {})
214+
# def test_placeholders_trailing_trim(self):
215+
# PH = self.module.Placeholder
216+
# for args, call_args, expected_args in [
217+
# [(PH,), (), ()],
218+
# [(0, PH), (), (0,)],
219+
# [(0, PH, 1, PH, PH, PH), (2,), (0, 2, 1)]
220+
# ]:
221+
# actual_args, actual_kwds = self.partial(capture, *args)(*call_args)
222+
# self.assertEqual(actual_args, expected_args)
223+
# self.assertEqual(actual_kwds, {})
224224

225225
def test_placeholders(self):
226226
PH = self.module.Placeholder
@@ -373,11 +373,11 @@ def test_setstate(self):
373373
f()
374374
self.assertEqual(f(2), ((2, 1), dict(a=10)))
375375

376-
# Trailing Placeholder error
377-
f = self.partial(signature)
378-
msg_regex = re.escape("unexpected trailing Placeholders")
379-
with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm:
380-
f.__setstate__((capture, (1, PH), dict(a=10), dict(attr=[])))
376+
# # Trailing Placeholder error
377+
# f = self.partial(signature)
378+
# msg_regex = re.escape("unexpected trailing Placeholders")
379+
# with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm:
380+
# f.__setstate__((capture, (1, PH), dict(a=10), dict(attr=[])))
381381

382382
def test_setstate_errors(self):
383383
f = self.partial(signature)

Lib/test/test_inspect/test_inspect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3671,7 +3671,7 @@ def test():
36713671
self.assertEqual(self.signature(Spam.ham, eval_str=False),
36723672
((), Ellipsis))
36733673
with self.assertRaisesRegex(ValueError, "invalid method signature"):
3674-
inspect.signature(Spam().ham)
3674+
inspect.signature(Spam().ham)
36753675

36763676
class Spam:
36773677
def test(it, a, b, *, c) -> 'spam':

Modules/_functoolsmodule.c

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,7 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
213213
pto->fn = Py_NewRef(func);
214214
pto->placeholder = phold;
215215

216-
/* Get new_args with trailing Placeholders trimmed */
217-
while (new_nargs > 0 && PyTuple_GET_ITEM(args, new_nargs) == phold) {
218-
new_nargs--;
219-
}
220-
221216
/* process args */
222-
Py_ssize_t phcount = 0;
223217
if (new_nargs == 0) {
224218
if (pto_args == NULL) {
225219
pto->args = PyTuple_New(0);
@@ -234,7 +228,8 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
234228
}
235229
else {
236230
/* Count placeholders */
237-
for (Py_ssize_t i = 0; i < new_nargs - 1; i++) {
231+
Py_ssize_t phcount = 0;
232+
for (Py_ssize_t i = 0; i < new_nargs; i++) {
238233
if (PyTuple_GET_ITEM(args, i + 1) == phold) {
239234
phcount++;
240235
}
@@ -414,6 +409,12 @@ partial_vectorcall(partialobject *pto, PyObject *const *args,
414409
pto_args, pto_nargs, NULL);
415410
}
416411

412+
/* Fast path if all Placeholders */
413+
if (pto_nargs == pto_phcount) {
414+
return _PyObject_VectorcallTstate(tstate, pto->fn,
415+
args, nargs, kwnames);
416+
}
417+
417418
/* Fast path using PY_VECTORCALL_ARGUMENTS_OFFSET to prepend a single
418419
* positional argument */
419420
if (pto_nargs == 1 && (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET)) {
@@ -701,11 +702,6 @@ partial_setstate(partialobject *pto, PyObject *state)
701702
return NULL;
702703
}
703704

704-
Py_ssize_t nargs = PyTuple_GET_SIZE(fnargs);
705-
if (nargs && PyTuple_GET_ITEM(fnargs, nargs - 1) == pto->placeholder) {
706-
PyErr_SetString(PyExc_TypeError, "unexpected trailing Placeholders");
707-
return NULL;
708-
}
709705
/* Count placeholders */
710706
Py_ssize_t phcount = 0;
711707
for (Py_ssize_t i = 0; i < nargs - 1; i++) {

0 commit comments

Comments
 (0)