Skip to content

Commit 707b957

Browse files
committed
Merge branch 'implement-119127' into implement-119127-again
2 parents 13a5fdc + 38d9c11 commit 707b957

File tree

5 files changed

+263
-16
lines changed

5 files changed

+263
-16
lines changed

Doc/library/functools.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,18 @@ The :mod:`functools` module defines the following functions:
358358
>>> basetwo('10010')
359359
18
360360

361+
If ``Placeholder`` sentinels are present in *args*, they will be filled first
362+
when :func:`partial` is called. This allows custom selection of positional arguments
363+
to be pre-filled when constructing :ref:`partial object<partial-objects>`.
364+
If ``Placeholder`` sentinels are used, all of them must be filled at call time.:
365+
366+
>>> from functools import partial, Placeholder
367+
>>> say_to_world = partial(print, Placeholder, 'world!')
368+
>>> say_to_world('Hello')
369+
Hello world!
370+
371+
.. versionchanged:: 3.14
372+
Support for ``Placeholder`` in *args*
361373

362374
.. class:: partialmethod(func, /, *args, **keywords)
363375

Lib/functools.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ def reduce(function, sequence, initial=_initial_missing):
273273
### partial() argument application
274274
################################################################################
275275

276+
277+
class Placeholder:
278+
"""placeholder for partial arguments"""
279+
280+
276281
# Purely functional, no descriptor behaviour
277282
class partial:
278283
"""New function with partial application of the given arguments
@@ -285,8 +290,33 @@ def __new__(cls, func, /, *args, **keywords):
285290
if not callable(func):
286291
raise TypeError("the first argument must be callable")
287292

293+
np = 0
294+
nargs = len(args)
295+
if args:
296+
while nargs and args[nargs-1] is Placeholder:
297+
nargs -= 1
298+
args = args[:nargs]
299+
np = args.count(Placeholder)
288300
if isinstance(func, partial):
289-
args = func.args + args
301+
pargs = func.args
302+
pnp = func.np
303+
if pnp and args:
304+
all_args = list(pargs)
305+
nargs = len(args)
306+
j, pos = 0, 0
307+
end = nargs if nargs < pnp else pnp
308+
while j < end:
309+
pos = all_args.index(Placeholder, pos)
310+
all_args[pos] = args[j]
311+
j += 1
312+
pos += 1
313+
if pnp < nargs:
314+
all_args.extend(args[pnp:])
315+
np += pnp - end
316+
args = tuple(all_args)
317+
else:
318+
np += pnp
319+
args = func.args + args
290320
keywords = {**func.keywords, **keywords}
291321
func = func.func
292322

@@ -295,11 +325,27 @@ def __new__(cls, func, /, *args, **keywords):
295325
self.func = func
296326
self.args = args
297327
self.keywords = keywords
328+
self.np = np
298329
return self
299330

300331
def __call__(self, /, *args, **keywords):
301-
keywords = {**self.keywords, **keywords}
302-
return self.func(*self.args, *args, **keywords)
332+
if not hasattr(self, 'np'):
333+
self.np = self.args.count(Placeholder)
334+
if np := self.np:
335+
if len(args) < np:
336+
raise TypeError("unfilled placeholders in 'partial' call")
337+
f_args = list(self.args)
338+
j, pos = 0, 0
339+
while j < np:
340+
pos = f_args.index(Placeholder, pos)
341+
f_args[pos] = args[j]
342+
j += 1
343+
pos += 1
344+
keywords = {**self.keywords, **keywords}
345+
return self.func(*f_args, *args[np:], **keywords)
346+
else:
347+
keywords = {**self.keywords, **keywords}
348+
return self.func(*self.args, *args, **keywords)
303349

304350
@recursive_repr()
305351
def __repr__(self):
@@ -340,7 +386,7 @@ def __setstate__(self, state):
340386
self.keywords = kwds
341387

342388
try:
343-
from _functools import partial
389+
from _functools import partial, Placeholder
344390
except ImportError:
345391
pass
346392

Lib/test/test_functools.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,43 @@ def foo(bar):
210210
p2.new_attr = 'spam'
211211
self.assertEqual(p2.new_attr, 'spam')
212212

213+
def test_placeholders_trailing_trim(self):
214+
PH = self.module.Placeholder
215+
for args in [(PH,), (PH, 0), (0, PH), (0, PH, 1, PH, PH, PH)]:
216+
expected, n = tuple(args), len(args)
217+
while n and args[n-1] is PH:
218+
n -= 1
219+
expected = expected[:n]
220+
p = self.partial(capture, *args)
221+
self.assertTrue(p.args == expected)
222+
223+
def test_placeholders(self):
224+
PH = self.module.Placeholder
225+
# 1 Placeholder
226+
args = (PH, 0)
227+
p = self.partial(capture, *args)
228+
got, empty = p('x')
229+
self.assertTrue(('x', 0) == got and empty == {})
230+
# 2 Placeholders
231+
args = (PH, 0, PH, 1)
232+
p = self.partial(capture, *args)
233+
with self.assertRaises(TypeError):
234+
got, empty = p('x')
235+
got, empty = p('x', 'y')
236+
expected = ('x', 0, 'y', 1)
237+
self.assertTrue(expected == got and empty == {})
238+
239+
def test_placeholders_optimization(self):
240+
PH = self.module.Placeholder
241+
p = self.partial(capture, PH, 0)
242+
p2 = self.partial(p, PH, 1, 2, 3)
243+
expected = (PH, 0, 1, 2, 3)
244+
self.assertTrue(expected == p2.args)
245+
p3 = self.partial(p2, -1, 4)
246+
got, empty = p3(5)
247+
expected = (-1, 0, 1, 2, 3, 4, 5)
248+
self.assertTrue(expected == got and empty == {})
249+
213250
def test_repr(self):
214251
args = (object(), object())
215252
args_repr = ', '.join(repr(a) for a in args)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``partial`` of ``functools`` module now supports placeholders for positional
2+
arguments.

0 commit comments

Comments
 (0)