From 2cfd159fee786e63e102b088266fd53e1b909869 Mon Sep 17 00:00:00 2001 From: Steven Barker Date: Mon, 30 May 2016 03:29:56 -0700 Subject: [PATCH] Add 3.5's apply_defaults method to BoundArguments --- README.rst | 39 +++++++++++++++--------------- funcsigs/__init__.py | 28 ++++++++++++++++++++++ tests/test_inspect.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 5fbca27..3e879bf 100644 --- a/README.rst +++ b/README.rst @@ -273,25 +273,8 @@ function. Arguments for which :meth:`Signature.bind` or :meth:`Signature.bind_partial` relied on a default value are skipped. - However, if needed, it is easy to include them. - - :: - - >>> def foo(a, b=10): - ... pass - - >>> sig = signature(foo) - >>> ba = sig.bind(5) - - >>> ba.args, ba.kwargs - ((5,), {}) - - >>> for param in sig.parameters.values(): - ... if param.name not in ba.arguments: - ... ba.arguments[param.name] = param.default - - >>> ba.args, ba.kwargs - ((5, 10), {}) + However, if needed, use :meth:`BoundArguments.apply_defaults` to add + them. .. attribute:: BoundArguments.args @@ -303,6 +286,24 @@ function. A dict of keyword arguments values. Dynamically computed from the :attr:`arguments` attribute. + + .. method:: BoundArguments.apply_defaults() + + Set default values for missing arguments. + + For variable-positional arguments (``*args``) the default is an + empty tuple. + + For variable-keyword arguments (``**kwargs``) the default is an + empty dict. + + :: + + >>> def foo(a, b='ham', *args): pass + >>> ba = inspect.signature(foo).bind('spam') + >>> ba.apply_defaults() + >>> ba.arguments + OrderedDict([('a', 'spam'), ('b', 'ham'), ('args', ())]) The :attr:`args` and :attr:`kwargs` properties can be used to invoke functions:: diff --git a/funcsigs/__init__.py b/funcsigs/__init__.py index 5f5378b..9ef3616 100644 --- a/funcsigs/__init__.py +++ b/funcsigs/__init__.py @@ -439,6 +439,34 @@ def kwargs(self): return kwargs + def apply_defaults(self): + """Set default values for missing arguments. + + For variable-positional arguments (*args) the default is an + empty tuple. + + For variable-keyword arguments (**kwargs) the default is an + empty dict. + """ + arguments = self.arguments + new_arguments = [] + for name, param in self._signature.parameters.items(): + try: + new_arguments.append((name, arguments[name])) + except KeyError: + if param.default is not _empty: + val = param.default + elif param.kind is _VAR_POSITIONAL: + val = () + elif param.kind is _VAR_KEYWORD: + val = {} + else: + # This BoundArguments was likely produced by + # Signature.bind_partial(). + continue + new_arguments.append((name, val)) + self.arguments = OrderedDict(new_arguments) + def __hash__(self): msg = "unhashable type: '{0}'".format(self.__class__.__name__) raise TypeError(msg) diff --git a/tests/test_inspect.py b/tests/test_inspect.py index 98d6592..69027cd 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -1000,3 +1000,58 @@ def foo(a): pass def bar(b): pass ba4 = inspect.signature(bar).bind(1) self.assertNotEqual(ba, ba4) + + if sys.version_info[0] > 2: + exec(""" +def test_signature_bound_arguments_apply_defaults(self): + def foo(a, b=1, *args, c:1={}, **kw): pass + sig = inspect.signature(foo) + + ba = sig.bind(20) + ba.apply_defaults() + self.assertEqual( + list(ba.arguments.items()), + [('a', 20), ('b', 1), ('args', ()), ('c', {}), ('kw', {})]) + + # Make sure that we preserve the order: + # i.e. 'c' should be *before* 'kw'. + ba = sig.bind(10, 20, 30, d=1) + ba.apply_defaults() + self.assertEqual( + list(ba.arguments.items()), + [('a', 10), ('b', 20), ('args', (30,)), ('c', {}), ('kw', {'d':1})]) +""") + + def test_signature_bound_arguments_apply_defaults_common(self): + def foo(a, b=1, *args, **kw): pass + sig = inspect.signature(foo) + + ba = sig.bind(20) + ba.apply_defaults() + self.assertEqual( + list(ba.arguments.items()), + [('a', 20), ('b', 1), ('args', ()), ('kw', {})]) + + # Make sure that BoundArguments produced by bind_partial() + # are supported. + def foo(a, b): pass + sig = inspect.signature(foo) + ba = sig.bind_partial(20) + ba.apply_defaults() + self.assertEqual( + list(ba.arguments.items()), + [('a', 20)]) + + # Test no args + def foo(): pass + sig = inspect.signature(foo) + ba = sig.bind() + ba.apply_defaults() + self.assertEqual(list(ba.arguments.items()), []) + + # Make sure a no-args binding still acquires proper defaults. + def foo(a='spam'): pass + sig = inspect.signature(foo) + ba = sig.bind() + ba.apply_defaults() + self.assertEqual(list(ba.arguments.items()), [('a', 'spam')])