diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -501,6 +501,15 @@ function. Returns :class:`BoundArguments` if ``*args`` and ``**kwargs`` match the signature, or raises a :exc:`TypeError`. + .. method:: Signature.bind_with_defaults(*args, **kwargs) + + Create a mapping from positional and keyword arguments to parameters. + Returns :class:`BoundArguments` if ``*args`` and ``**kwargs`` match the + signature, or raises a :exc:`TypeError`. Arguments that are not specified + but have a default value will be filled in too. + + .. versionadded:: 3.5 + .. method:: Signature.bind_partial(*args, **kwargs) Works the same way as :meth:`Signature.bind`, but allows the omission of diff --git a/Lib/inspect.py b/Lib/inspect.py --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2690,6 +2690,25 @@ class Signature: """ return args[0]._bind(args[1:], kwargs) + def bind_with_defaults(*args, **kwargs): + """Get a BoundArguments object, that maps the passed `args` + and `kwargs` to the function's signature. Raises `TypeError` + if the passed arguments can not be bound. Arguments that are not + specified but have a default will be filled in too. + """ + boundargs = args[0]._bind(args[1:], kwargs) + + for param in args[0].parameters.values(): + if param.name not in boundargs.arguments: + if param.kind is Parameter.VAR_POSITIONAL: + default = () + elif param.kind is Parameter.VAR_KEYWORD: + default = {} + else: + default = param.default + boundargs.arguments[param.name] = default + return boundargs + def bind_partial(*args, **kwargs): """Get a BoundArguments object, that partially maps the passed `args` and `kwargs` to the function's signature. diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3059,6 +3059,35 @@ class TestSignatureBind(unittest.TestCas self.assertEqual(ba.arguments, {'kwargs': {'args': 1}}) +class TestSignatureBindWithDefaults(unittest.TestCase): + @staticmethod + def call(func, *args, **kwargs): + sig = inspect.signature(func) + ba = sig.bind_with_defaults(*args, **kwargs) + return func(*ba.args, **ba.kwargs) + + def test_signature_bind_default(self): + def test(a=17, b=23): + return a, b + + self.assertEqual(self.call(test), (17, 23)) + self.assertEqual(self.call(test, 18), (18, 23)) + self.assertEqual(self.call(test, 18, 24), (18, 24)) + + def test_signature_bind_var(self): + def test(*args, **kwargs): + return args, kwargs + + self.assertEqual(self.call(test), ((), {})) + self.assertEqual(self.call(test, 1), ((1,), {})) + self.assertEqual(self.call(test, 1, 2), ((1, 2), {})) + self.assertEqual(self.call(test, foo='bar'), ((), {'foo': 'bar'})) + self.assertEqual(self.call(test, 1, foo='bar'), ((1,), {'foo': 'bar'})) + self.assertEqual(self.call(test, args=10), ((), {'args': 10})) + self.assertEqual(self.call(test, 1, 2, foo='bar'), + ((1, 2), {'foo': 'bar'})) + + class TestBoundArguments(unittest.TestCase): def test_signature_bound_arguments_unhashable(self): def foo(a): pass