Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit da5fe4f

Browse files
committed
inspect.signature: Add support for 'functools.partialmethod' #20223
1 parent eedf1c1 commit da5fe4f

3 files changed

Lines changed: 95 additions & 40 deletions

File tree

Lib/functools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def _method(*args, **keywords):
290290
call_args = (cls_or_self,) + self.args + tuple(rest)
291291
return self.func(*call_args, **call_keywords)
292292
_method.__isabstractmethod__ = self.__isabstractmethod__
293+
_method._partialmethod = self
293294
return _method
294295

295296
def __get__(self, obj, cls):

Lib/inspect.py

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,51 @@ def _get_user_defined_method(cls, method_name):
14401440
return meth
14411441

14421442

1443+
def _get_partial_signature(wrapped_sig, partial, extra_args=()):
1444+
new_params = OrderedDict(wrapped_sig.parameters.items())
1445+
1446+
partial_args = partial.args or ()
1447+
partial_keywords = partial.keywords or {}
1448+
1449+
if extra_args:
1450+
partial_args = extra_args + partial_args
1451+
1452+
try:
1453+
ba = wrapped_sig.bind_partial(*partial_args, **partial_keywords)
1454+
except TypeError as ex:
1455+
msg = 'partial object {!r} has incorrect arguments'.format(partial)
1456+
raise ValueError(msg) from ex
1457+
1458+
for arg_name, arg_value in ba.arguments.items():
1459+
param = new_params[arg_name]
1460+
if arg_name in partial_keywords:
1461+
# We set a new default value, because the following code
1462+
# is correct:
1463+
#
1464+
# >>> def foo(a): print(a)
1465+
# >>> print(partial(partial(foo, a=10), a=20)())
1466+
# 20
1467+
# >>> print(partial(partial(foo, a=10), a=20)(a=30))
1468+
# 30
1469+
#
1470+
# So, with 'partial' objects, passing a keyword argument is
1471+
# like setting a new default value for the corresponding
1472+
# parameter
1473+
#
1474+
# We also mark this parameter with '_partial_kwarg'
1475+
# flag. Later, in '_bind', the 'default' value of this
1476+
# parameter will be added to 'kwargs', to simulate
1477+
# the 'functools.partial' real call.
1478+
new_params[arg_name] = param.replace(default=arg_value,
1479+
_partial_kwarg=True)
1480+
1481+
elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
1482+
not param._partial_kwarg):
1483+
new_params.pop(arg_name)
1484+
1485+
return wrapped_sig.replace(parameters=new_params.values())
1486+
1487+
14431488
def signature(obj):
14441489
'''Get a signature object for the passed callable.'''
14451490

@@ -1470,50 +1515,32 @@ def signature(obj):
14701515
if sig is not None:
14711516
return sig
14721517

1518+
try:
1519+
partialmethod = obj._partialmethod
1520+
except AttributeError:
1521+
pass
1522+
else:
1523+
# Unbound partialmethod (see functools.partialmethod)
1524+
# This means, that we need to calculate the signature
1525+
# as if it's a regular partial object, but taking into
1526+
# account that the first positional argument
1527+
# (usually `self`, or `cls`) will not be passed
1528+
# automatically (as for boundmethods)
1529+
1530+
wrapped_sig = signature(partialmethod.func)
1531+
sig = _get_partial_signature(wrapped_sig, partialmethod, (None,))
1532+
1533+
first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
1534+
new_params = (first_wrapped_param,) + tuple(sig.parameters.values())
1535+
1536+
return sig.replace(parameters=new_params)
1537+
14731538
if isinstance(obj, types.FunctionType):
14741539
return Signature.from_function(obj)
14751540

14761541
if isinstance(obj, functools.partial):
1477-
sig = signature(obj.func)
1478-
1479-
new_params = OrderedDict(sig.parameters.items())
1480-
1481-
partial_args = obj.args or ()
1482-
partial_keywords = obj.keywords or {}
1483-
try:
1484-
ba = sig.bind_partial(*partial_args, **partial_keywords)
1485-
except TypeError as ex:
1486-
msg = 'partial object {!r} has incorrect arguments'.format(obj)
1487-
raise ValueError(msg) from ex
1488-
1489-
for arg_name, arg_value in ba.arguments.items():
1490-
param = new_params[arg_name]
1491-
if arg_name in partial_keywords:
1492-
# We set a new default value, because the following code
1493-
# is correct:
1494-
#
1495-
# >>> def foo(a): print(a)
1496-
# >>> print(partial(partial(foo, a=10), a=20)())
1497-
# 20
1498-
# >>> print(partial(partial(foo, a=10), a=20)(a=30))
1499-
# 30
1500-
#
1501-
# So, with 'partial' objects, passing a keyword argument is
1502-
# like setting a new default value for the corresponding
1503-
# parameter
1504-
#
1505-
# We also mark this parameter with '_partial_kwarg'
1506-
# flag. Later, in '_bind', the 'default' value of this
1507-
# parameter will be added to 'kwargs', to simulate
1508-
# the 'functools.partial' real call.
1509-
new_params[arg_name] = param.replace(default=arg_value,
1510-
_partial_kwarg=True)
1511-
1512-
elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
1513-
not param._partial_kwarg):
1514-
new_params.pop(arg_name)
1515-
1516-
return sig.replace(parameters=new_params.values())
1542+
wrapped_sig = signature(obj.func)
1543+
return _get_partial_signature(wrapped_sig, obj)
15171544

15181545
sig = None
15191546
if isinstance(obj, type):

Lib/test/test_inspect.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,33 @@ def foo(a=1, b=2, c=3):
18771877
ba = inspect.signature(_foo).bind(12, 14)
18781878
self.assertEqual(_foo(*ba.args, **ba.kwargs), (12, 14, 13))
18791879

1880+
def test_signature_on_partialmethod(self):
1881+
from functools import partialmethod
1882+
1883+
class Spam:
1884+
def test():
1885+
pass
1886+
ham = partialmethod(test)
1887+
1888+
with self.assertRaisesRegex(ValueError, "has incorrect arguments"):
1889+
inspect.signature(Spam.ham)
1890+
1891+
class Spam:
1892+
def test(it, a, *, c) -> 'spam':
1893+
pass
1894+
ham = partialmethod(test, c=1)
1895+
1896+
self.assertEqual(self.signature(Spam.ham),
1897+
((('it', ..., ..., 'positional_or_keyword'),
1898+
('a', ..., ..., 'positional_or_keyword'),
1899+
('c', 1, ..., 'keyword_only')),
1900+
'spam'))
1901+
1902+
self.assertEqual(self.signature(Spam().ham),
1903+
((('a', ..., ..., 'positional_or_keyword'),
1904+
('c', 1, ..., 'keyword_only')),
1905+
'spam'))
1906+
18801907
def test_signature_on_decorated(self):
18811908
import functools
18821909

0 commit comments

Comments
 (0)