diff --git a/boltons/funcutils.py b/boltons/funcutils.py index c3a11c6..d5b7f33 100644 --- a/boltons/funcutils.py +++ b/boltons/funcutils.py @@ -561,16 +561,24 @@ class FunctionBuilder(object): Raises a :exc:`ValueError` if the argument is not present. """ + args = self.args d_dict = self.get_defaults_dict() try: - self.args.remove(arg_name) + args.remove(arg_name) except ValueError: - exc = MissingArgument('arg %r not found in %s argument list: %r' - % (arg_name, self.name, self.args)) - exc.arg_name = arg_name - raise exc - d_dict.pop(arg_name, None) - self.defaults = tuple([d_dict[a] for a in self.args if a in d_dict]) + try: + self.kwonlyargs.remove(arg_name) + except (AttributeError, ValueError): + # py2, or py3 and missing from both + exc = MissingArgument('arg %r not found in %s argument list:' + ' %r' % (arg_name, self.name, args)) + exc.arg_name = arg_name + raise exc + else: + self.kwonlydefaults.pop(arg_name, None) + else: + d_dict.pop(arg_name, None) + self.defaults = tuple([d_dict[a] for a in args if a in d_dict]) return def _compile(self, src, execdict): diff --git a/tests/test_funcutils_fb_py3.py b/tests/test_funcutils_fb_py3.py index 1f5a50f..8664b10 100644 --- a/tests/test_funcutils_fb_py3.py +++ b/tests/test_funcutils_fb_py3.py @@ -1,7 +1,10 @@ -from boltons.funcutils import wraps, FunctionBuilder + +import inspect import pytest +from boltons.funcutils import wraps, FunctionBuilder + def pita_wrap(flag=False): @@ -42,6 +45,39 @@ def test_wraps_py3(): True, 'kwonly_non_roundtrippable_repr', 2) +def test_remove_kwonly_arg(): + # example adapted from https://github.com/mahmoud/boltons/issues/123 + + def darkhelm_inject_loop(func): + sig = inspect.signature(func) + loop_param = sig.parameters['loop'].replace(default=None) + sig = sig.replace(parameters=[loop_param]) + + def add_loop(args, kwargs): + bargs = sig.bind(*args, **kwargs) + bargs.apply_defaults() + if bargs.arguments['loop'] is None: + bargs.arguments['loop'] = "don't look at me, I just use gevent" + + return bargs.arguments + + def wrapper(*args, **kwargs): + return func(**add_loop(args, kwargs)) + + return wraps(func, injected=['loop'])(wrapper) + + @darkhelm_inject_loop + def example(test='default', *, loop='lol'): + return loop + + fb_example = FunctionBuilder.from_func(example) + assert 'test' in fb_example.args + assert fb_example.get_defaults_dict()['test'] == 'default' + + assert 'loop' not in fb_example.kwonlyargs + assert 'loop' not in fb_example.kwonlydefaults + + @pytest.mark.parametrize('signature,should_match', [('a, *, b', True), ('a,*,b', True),