Skip to content

Commit 9262323

Browse files
committed
fix
1 parent 2518918 commit 9262323

5 files changed

Lines changed: 185 additions & 195 deletions

File tree

Lib/functools.py

Lines changed: 102 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
1515
'cached_property', 'Placeholder']
1616

17-
from abc import get_cache_token
17+
from abc import abstractmethod, get_cache_token
1818
from collections import namedtuple
1919
# import types, weakref # Deferred to single_dispatch()
2020
from operator import itemgetter
2121
from reprlib import recursive_repr
22-
from types import MethodType
22+
from types import FunctionType, MethodType
2323
from _thread import RLock
2424

2525
# Avoid importing types, so we can speedup import time
@@ -316,49 +316,6 @@ def _partial_prepare_merger(args):
316316
merger = itemgetter(*order) if phcount else None
317317
return phcount, merger
318318

319-
def _partial_new(cls, func, /, *args, **keywords):
320-
if issubclass(cls, partial):
321-
base_cls = partial
322-
if not callable(func):
323-
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-
362319
def _partial_repr(self):
363320
cls = type(self)
364321
module = cls.__module__
@@ -377,7 +334,49 @@ class partial:
377334
__slots__ = ("func", "args", "keywords", "_phcount", "_merger",
378335
"__dict__", "__weakref__")
379336

380-
__new__ = _partial_new
337+
def __new__(cls, func, /, *args, **keywords):
338+
if not callable(func):
339+
raise TypeError("the first argument must be callable")
340+
if args and args[-1] is Placeholder:
341+
# Trim trailing placeholders
342+
j = len(args) - 1
343+
if not j:
344+
args = ()
345+
else:
346+
while (j := j - 1) >= 0:
347+
if args[j] is not Placeholder:
348+
break
349+
args = args[:j + 1]
350+
if isinstance(func, partial):
351+
pto_phcount = func._phcount
352+
tot_args = func.args
353+
if args:
354+
tot_args += args
355+
if pto_phcount:
356+
# merge args with args of `func` which is `partial`
357+
nargs = len(args)
358+
if nargs < pto_phcount:
359+
tot_args += (Placeholder,) * (pto_phcount - nargs)
360+
tot_args = func._merger(tot_args)
361+
if nargs > pto_phcount:
362+
tot_args += args[pto_phcount:]
363+
phcount, merger = _partial_prepare_merger(tot_args)
364+
else: # works for both pto_phcount == 0 and != 0
365+
phcount, merger = pto_phcount, func._merger
366+
keywords = {**func.keywords, **keywords}
367+
func = func.func
368+
else:
369+
tot_args = args
370+
phcount, merger = _partial_prepare_merger(tot_args)
371+
372+
self = object.__new__(cls)
373+
self.func = func
374+
self.args = tot_args
375+
self.keywords = keywords
376+
self._phcount = phcount
377+
self._merger = merger
378+
return self
379+
381380
__repr__ = recursive_repr()(_partial_repr)
382381

383382
def __call__(self, /, *args, **keywords):
@@ -416,7 +415,7 @@ def __setstate__(self, state):
416415
raise TypeError("invalid partial state")
417416

418417
if args and args[-1] is Placeholder:
419-
raise TypeError("trailing Placeholders are not allowed")
418+
raise TypeError("unexpected trailing Placeholders")
420419
phcount, merger = _partial_prepare_merger(args)
421420

422421
args = tuple(args) # just in case it's a subclass
@@ -439,6 +438,7 @@ def __setstate__(self, state):
439438
except ImportError:
440439
pass
441440

441+
442442
# Descriptor version
443443
class partialmethod:
444444
"""Method descriptor with partial application of the given arguments
@@ -447,50 +447,65 @@ class partialmethod:
447447
Supports wrapping existing descriptors and handles non-descriptor
448448
callables as instance methods.
449449
"""
450-
__new__ = _partial_new
450+
451+
__slots__ = ("func", "args", "keywords", "wrapper",
452+
"__isabstractmethod__", "__dict__", "__weakref__")
451453
__repr__ = _partial_repr
452454

453-
def _make_unbound_method(self):
454-
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
466-
keywords = {**self.keywords, **keywords}
467-
return self.func(cls_or_self, *pto_args, *args, **keywords)
468-
_method.__isabstractmethod__ = self.__isabstractmethod__
469-
_method.__partialmethod__ = self
470-
return _method
455+
def __init__(self, func, /, *args, **keywords):
456+
if isinstance(func, partialmethod):
457+
# Subclass optimization
458+
temp = partial(lambda: None, *func.args, **func.keywords)
459+
temp = partial(temp, *args, **keywords)
460+
func = func.func
461+
args = temp.args
462+
keywords = temp.keywords
463+
self.func = func
464+
self.args = args
465+
self.keywords = keywords
466+
self.__isabstractmethod__ = getattr(func, "__isabstractmethod__", False)
467+
468+
# 5 cases
469+
rewrap = None
470+
if isinstance(func, staticmethod):
471+
self.wrapper = partial(func.__wrapped__, *args, **keywords)
472+
rewrap = staticmethod
473+
elif isinstance(func, classmethod):
474+
self.wrapper = partial(func.__wrapped__, Placeholder, *args, **keywords)
475+
rewrap = classmethod
476+
elif isinstance(func, (FunctionType, partial)):
477+
# instance method
478+
self.wrapper = partial(func, Placeholder, *args, **keywords)
479+
elif getattr(func, '__get__', None) is None:
480+
if not callable(func):
481+
raise TypeError(f"the first argument {func!r} must be a callable "
482+
"or a descriptor")
483+
# callable object without __get__
484+
# treat this like an instance method
485+
self.wrapper = partial(func, Placeholder, *args, **keywords)
486+
else:
487+
# Unknown descriptor
488+
self.wrapper = None
471489

472-
def __get__(self, obj, cls=None):
473-
get = getattr(self.func, "__get__", None)
474-
result = None
475-
if get is not None:
476-
new_func = get(obj, cls)
477-
if new_func is not self.func:
478-
# Assume __get__ returning something new indicates the
479-
# creation of an appropriate callable
480-
result = partial(new_func, *self.args, **self.keywords)
481-
try:
482-
result.__self__ = new_func.__self__
483-
except AttributeError:
484-
pass
485-
if result is None:
486-
# If the underlying descriptor didn't do anything, treat this
487-
# like an instance method
488-
result = self._make_unbound_method().__get__(obj, cls)
489-
return result
490+
# Adjust for abstract and rewrap if needed
491+
if self.wrapper is not None:
492+
if self.__isabstractmethod__:
493+
self.wrapper = abstractmethod(self.wrapper)
494+
if rewrap is not None:
495+
self.wrapper = rewrap(self.wrapper)
490496

491-
@property
492-
def __isabstractmethod__(self):
493-
return getattr(self.func, "__isabstractmethod__", False)
497+
def __get__(self, obj, cls=None):
498+
if self.wrapper is not None:
499+
return self.wrapper.__get__(obj, cls)
500+
else:
501+
# Unknown descriptor
502+
new_func = getattr(self.func, '__get__')(obj, cls)
503+
result = partial(new_func, *self.args, **self.keywords)
504+
try:
505+
result.__self__ = new_func.__self__
506+
except AttributeError:
507+
pass
508+
return result
494509

495510
__class_getitem__ = classmethod(GenericAlias)
496511

@@ -506,8 +521,6 @@ def _unwrap_partialmethod(func):
506521
prev = None
507522
while func is not prev:
508523
prev = func
509-
while isinstance(getattr(func, "__partialmethod__", None), partialmethod):
510-
func = func.__partialmethod__
511524
while isinstance(func, partialmethod):
512525
func = getattr(func, 'func')
513526
func = _unwrap_partial(func)

Lib/inspect.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2436,40 +2436,8 @@ def _signature_from_callable(obj, *,
24362436
'attribute'.format(o_sig))
24372437
return sig
24382438

2439-
try:
2440-
partialmethod = obj.__partialmethod__
2441-
except AttributeError:
2442-
pass
2443-
else:
2444-
if isinstance(partialmethod, functools.partialmethod):
2445-
# Unbound partialmethod (see functools.partialmethod)
2446-
# This means, that we need to calculate the signature
2447-
# as if it's a regular partial object, but taking into
2448-
# account that the first positional argument
2449-
# (usually `self`, or `cls`) will not be passed
2450-
# automatically (as for boundmethods)
2451-
2452-
wrapped_sig = _get_signature_of(partialmethod.func)
2453-
2454-
sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
2455-
first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
2456-
if first_wrapped_param.kind is Parameter.VAR_POSITIONAL:
2457-
# First argument of the wrapped callable is `*args`, as in
2458-
# `partialmethod(lambda *args)`.
2459-
return sig
2460-
else:
2461-
sig_params = tuple(sig.parameters.values())
2462-
assert (not sig_params or
2463-
first_wrapped_param is not sig_params[0])
2464-
# If there were placeholders set,
2465-
# first param is transformed to positional only
2466-
if partialmethod.args.count(functools.Placeholder):
2467-
first_wrapped_param = first_wrapped_param.replace(
2468-
kind=Parameter.POSITIONAL_ONLY)
2469-
new_params = (first_wrapped_param,) + sig_params
2470-
return sig.replace(parameters=new_params)
2471-
24722439
if isinstance(obj, functools.partial):
2440+
print('HERE?', obj)
24732441
wrapped_sig = _get_signature_of(obj.func)
24742442
return _signature_get_partial(wrapped_sig, obj)
24752443

Lib/test/test_functools.py

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

214-
def test_placeholders_trailing_raise(self):
214+
def test_placeholders_trailing_trim(self):
215215
PH = self.module.Placeholder
216-
for args in [(PH,), (0, PH), (0, PH, 1, PH, PH, PH)]:
217-
with self.assertRaises(TypeError):
218-
self.partial(capture, *args)
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, {})
219224

220225
def test_placeholders(self):
221226
PH = self.module.Placeholder
@@ -370,7 +375,7 @@ def test_setstate(self):
370375

371376
# Trailing Placeholder error
372377
f = self.partial(signature)
373-
msg_regex = re.escape("trailing Placeholders are not allowed")
378+
msg_regex = re.escape("unexpected trailing Placeholders")
374379
with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm:
375380
f.__setstate__((capture, (1, PH), dict(a=10), dict(attr=[])))
376381

@@ -713,6 +718,7 @@ class PartialMethodSubclass(functools.partialmethod):
713718
p = functools.partialmethod(min, 2)
714719
p2 = PartialMethodSubclass(p, 1)
715720
self.assertIs(p2.func, min)
721+
print(p2.__get__(0)())
716722
self.assertEqual(p2.__get__(0)(), 0)
717723
# `partialmethod` subclass input to `partialmethod` subclass
718724
p = PartialMethodSubclass(min, 2)

Lib/test/test_inspect/test_inspect.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3606,14 +3606,6 @@ def foo(a=0, b=1, /, c=2, d=3):
36063606
def test_signature_on_partialmethod(self):
36073607
from functools import partialmethod
36083608

3609-
class Spam:
3610-
def test():
3611-
pass
3612-
ham = partialmethod(test)
3613-
3614-
with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
3615-
inspect.signature(Spam.ham)
3616-
36173609
class Spam:
36183610
def test(it, a, b, *, c) -> 'spam':
36193611
pass
@@ -3651,14 +3643,9 @@ def test(self: 'anno', x):
36513643
g = partialmethod(test, 1)
36523644

36533645
self.assertEqual(self.signature(Spam.g, eval_str=False),
3654-
((('self', ..., 'anno', 'positional_or_keyword'),),
3646+
((('self', ..., 'anno', 'positional_only'),),
36553647
...))
36563648

3657-
def test_signature_on_fake_partialmethod(self):
3658-
def foo(a): pass
3659-
foo.__partialmethod__ = 'spam'
3660-
self.assertEqual(str(inspect.signature(foo)), '(a)')
3661-
36623649
def test_signature_on_decorated(self):
36633650
def decorator(func):
36643651
@functools.wraps(func)

0 commit comments

Comments
 (0)