66# Written by Nick Coghlan <ncoghlan at gmail.com>,
77# Raymond Hettinger <python at rcn.com>,
88# and Łukasz Langa <lukasz at langa.pl>.
9- # Copyright (C) 2006-2013 Python Software Foundation.
9+ # Copyright (C) 2006-2024 Python Software Foundation.
1010# See C source code for _functools credits/copyright
1111
1212__all__ = ['update_wrapper' , 'wraps' , 'WRAPPER_ASSIGNMENTS' , 'WRAPPER_UPDATES' ,
1313 'total_ordering' , 'cache' , 'cmp_to_key' , 'lru_cache' , 'reduce' ,
1414 'partial' , 'partialmethod' , 'singledispatch' , 'singledispatchmethod' ,
15- 'cached_property' ]
15+ 'cached_property' , 'Placeholder' ]
1616
1717from abc import get_cache_token
1818from collections import namedtuple
1919# import types, weakref # Deferred to single_dispatch()
20+ from operator import itemgetter
2021from reprlib import recursive_repr
2122from types import MethodType
2223from _thread import RLock
@@ -274,43 +275,125 @@ def reduce(function, sequence, initial=_initial_missing):
274275### partial() argument application
275276################################################################################
276277
277- # Purely functional, no descriptor behaviour
278- class partial :
279- """New function with partial application of the given arguments
280- and keywords.
278+
279+ class _PlaceholderType :
280+ """The type of the Placeholder singleton.
281+
282+ Used as a placeholder for partial arguments.
281283 """
284+ __instance = None
285+ __slots__ = ()
286+
287+ def __init_subclass__ (cls , * args , ** kwargs ):
288+ raise TypeError (f"type '{ cls .__name__ } ' is not an acceptable base type" )
282289
283- __slots__ = "func" , "args" , "keywords" , "__dict__" , "__weakref__"
290+ def __new__ (cls ):
291+ if cls .__instance is None :
292+ cls .__instance = object .__new__ (cls )
293+ return cls .__instance
294+
295+ def __repr__ (self ):
296+ return 'Placeholder'
284297
285- def __new__ (cls , func , / , * args , ** keywords ):
298+ def __reduce__ (self ):
299+ return 'Placeholder'
300+
301+ Placeholder = _PlaceholderType ()
302+
303+ def _partial_prepare_merger (args ):
304+ if not args :
305+ return 0 , None
306+ nargs = len (args )
307+ order = []
308+ j = nargs
309+ for i , a in enumerate (args ):
310+ if a is Placeholder :
311+ order .append (j )
312+ j += 1
313+ else :
314+ order .append (i )
315+ phcount = j - nargs
316+ merger = itemgetter (* order ) if phcount else None
317+ return phcount , merger
318+
319+ def _partial_new (cls , func , / , * args , ** keywords ):
320+ if issubclass (cls , partial ):
321+ base_cls = partial
286322 if not callable (func ):
287323 raise TypeError ("the first argument must be callable" )
324+ else :
325+ base_cls = partialmethod
326+ # func could be a descriptor like classmethod which isn't callable
327+ if not callable (func ) and not hasattr (func , "__get__" ):
328+ raise TypeError (f"the first argument { func !r} must be a callable "
329+ "or a descriptor" )
330+ if args and args [- 1 ] is Placeholder :
331+ raise TypeError ("trailing Placeholders are not allowed" )
332+ if isinstance (func , base_cls ):
333+ pto_phcount = func ._phcount
334+ tot_args = func .args
335+ if args :
336+ tot_args += args
337+ if pto_phcount :
338+ # merge args with args of `func` which is `partial`
339+ nargs = len (args )
340+ if nargs < pto_phcount :
341+ tot_args += (Placeholder ,) * (pto_phcount - nargs )
342+ tot_args = func ._merger (tot_args )
343+ if nargs > pto_phcount :
344+ tot_args += args [pto_phcount :]
345+ phcount , merger = _partial_prepare_merger (tot_args )
346+ else : # works for both pto_phcount == 0 and != 0
347+ phcount , merger = pto_phcount , func ._merger
348+ keywords = {** func .keywords , ** keywords }
349+ func = func .func
350+ else :
351+ tot_args = args
352+ phcount , merger = _partial_prepare_merger (tot_args )
353+
354+ self = object .__new__ (cls )
355+ self .func = func
356+ self .args = tot_args
357+ self .keywords = keywords
358+ self ._phcount = phcount
359+ self ._merger = merger
360+ return self
361+
362+ def _partial_repr (self ):
363+ cls = type (self )
364+ module = cls .__module__
365+ qualname = cls .__qualname__
366+ args = [repr (self .func )]
367+ args .extend (map (repr , self .args ))
368+ args .extend (f"{ k } ={ v !r} " for k , v in self .keywords .items ())
369+ return f"{ module } .{ qualname } ({ ', ' .join (args )} )"
288370
289- if isinstance (func , partial ):
290- args = func .args + args
291- keywords = {** func .keywords , ** keywords }
292- func = func .func
371+ # Purely functional, no descriptor behaviour
372+ class partial :
373+ """New function with partial application of the given arguments
374+ and keywords.
375+ """
293376
294- self = super (partial , cls ).__new__ (cls )
377+ __slots__ = ("func" , "args" , "keywords" , "_phcount" , "_merger" ,
378+ "__dict__" , "__weakref__" )
295379
296- self .func = func
297- self .args = args
298- self .keywords = keywords
299- return self
380+ __new__ = _partial_new
381+ __repr__ = recursive_repr ()(_partial_repr )
300382
301383 def __call__ (self , / , * args , ** keywords ):
384+ phcount = self ._phcount
385+ if phcount :
386+ try :
387+ pto_args = self ._merger (self .args + args )
388+ args = args [phcount :]
389+ except IndexError :
390+ raise TypeError ("missing positional arguments "
391+ "in 'partial' call; expected "
392+ f"at least { phcount } , got { len (args )} " )
393+ else :
394+ pto_args = self .args
302395 keywords = {** self .keywords , ** keywords }
303- return self .func (* self .args , * args , ** keywords )
304-
305- @recursive_repr ()
306- def __repr__ (self ):
307- cls = type (self )
308- qualname = cls .__qualname__
309- module = cls .__module__
310- args = [repr (self .func )]
311- args .extend (repr (x ) for x in self .args )
312- args .extend (f"{ k } ={ v !r} " for (k , v ) in self .keywords .items ())
313- return f"{ module } .{ qualname } ({ ', ' .join (args )} )"
396+ return self .func (* pto_args , * args , ** keywords )
314397
315398 def __get__ (self , obj , objtype = None ):
316399 if obj is None :
@@ -332,6 +415,10 @@ def __setstate__(self, state):
332415 (namespace is not None and not isinstance (namespace , dict ))):
333416 raise TypeError ("invalid partial state" )
334417
418+ if args and args [- 1 ] is Placeholder :
419+ raise TypeError ("trailing Placeholders are not allowed" )
420+ phcount , merger = _partial_prepare_merger (args )
421+
335422 args = tuple (args ) # just in case it's a subclass
336423 if kwds is None :
337424 kwds = {}
@@ -344,53 +431,40 @@ def __setstate__(self, state):
344431 self .func = func
345432 self .args = args
346433 self .keywords = kwds
434+ self ._phcount = phcount
435+ self ._merger = merger
347436
348437try :
349- from _functools import partial
438+ from _functools import partial , Placeholder , _PlaceholderType
350439except ImportError :
351440 pass
352441
353442# Descriptor version
354- class partialmethod ( object ) :
443+ class partialmethod :
355444 """Method descriptor with partial application of the given arguments
356445 and keywords.
357446
358447 Supports wrapping existing descriptors and handles non-descriptor
359448 callables as instance methods.
360449 """
361-
362- def __init__ (self , func , / , * args , ** keywords ):
363- if not callable (func ) and not hasattr (func , "__get__" ):
364- raise TypeError ("{!r} is not callable or a descriptor"
365- .format (func ))
366-
367- # func could be a descriptor like classmethod which isn't callable,
368- # so we can't inherit from partial (it verifies func is callable)
369- if isinstance (func , partialmethod ):
370- # flattening is mandatory in order to place cls/self before all
371- # other arguments
372- # it's also more efficient since only one function will be called
373- self .func = func .func
374- self .args = func .args + args
375- self .keywords = {** func .keywords , ** keywords }
376- else :
377- self .func = func
378- self .args = args
379- self .keywords = keywords
380-
381- def __repr__ (self ):
382- cls = type (self )
383- module = cls .__module__
384- qualname = cls .__qualname__
385- args = [repr (self .func )]
386- args .extend (map (repr , self .args ))
387- args .extend (f"{ k } ={ v !r} " for k , v in self .keywords .items ())
388- return f"{ module } .{ qualname } ({ ', ' .join (args )} )"
450+ __new__ = _partial_new
451+ __repr__ = _partial_repr
389452
390453 def _make_unbound_method (self ):
391454 def _method (cls_or_self , / , * args , ** keywords ):
455+ phcount = self ._phcount
456+ if phcount :
457+ try :
458+ pto_args = self ._merger (self .args + args )
459+ args = args [phcount :]
460+ except IndexError :
461+ raise TypeError ("missing positional arguments "
462+ "in 'partialmethod' call; expected "
463+ f"at least { phcount } , got { len (args )} " )
464+ else :
465+ pto_args = self .args
392466 keywords = {** self .keywords , ** keywords }
393- return self .func (cls_or_self , * self . args , * args , ** keywords )
467+ return self .func (cls_or_self , * pto_args , * args , ** keywords )
394468 _method .__isabstractmethod__ = self .__isabstractmethod__
395469 _method .__partialmethod__ = self
396470 return _method
0 commit comments