import time import inspect import functools from collections import defaultdict import pytest from boltons.funcutils import wraps, FunctionBuilder, update_wrapper, copy_function import boltons.funcutils as funcutils def wrappable_func(a, b): return a, b def wrappable_varkw_func(a, b, **kw): return a, b def pita_wrap(flag=False): def cedar_dec(func): @wraps(func) def cedar_wrapper(*a, **kw): return (flag, func.__name__, func(*a, **kw)) return cedar_wrapper return cedar_dec def test_wraps_py3(): @pita_wrap(flag=True) def annotations(a: int, b: float=1, c: defaultdict=()) -> defaultdict: return a, b, c assert annotations(0) == (True, "annotations", (0, 1, ())) assert annotations.__annotations__ == {'a': int, 'b': float, 'c': defaultdict, 'return': defaultdict} @pita_wrap(flag=False) def kwonly_arg(a, *, b, c=2): return a, b, c with pytest.raises(TypeError): kwonly_arg(0) assert kwonly_arg(0, b=1) == (False, "kwonly_arg", (0, 1, 2)) assert kwonly_arg(0, b=1, c=3) == (False, "kwonly_arg", (0, 1, 3)) @pita_wrap(flag=True) def kwonly_non_roundtrippable_repr(*, x=lambda y: y + 1): return x(1) assert kwonly_non_roundtrippable_repr() == ( True, 'kwonly_non_roundtrippable_repr', 2) def test_copy_function_kw_defaults_py3(): # test that the copy works with keyword-only defaults f = lambda x, *, y=2: x * y f_copy = copy_function(f) assert f(21) == f_copy(21) == 42 @pytest.mark.parametrize('partial_kind', (functools, funcutils)) def test_update_wrapper_partial(partial_kind): wrapper = partial_kind.partial(wrappable_varkw_func, b=1) fully_wrapped = update_wrapper(wrapper, wrappable_varkw_func) assert fully_wrapped(1) == (1, 1) 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 def test_defaults_dict(): def example(req, test='default', *, loop='lol'): return loop fb_example = FunctionBuilder.from_func(example) assert 'test' in fb_example.args dd = fb_example.get_defaults_dict() assert dd['test'] == 'default' assert dd['loop'] == 'lol' assert 'req' not in dd def test_get_arg_names(): def example(req, test='default', *, loop='lol'): return loop fb_example = FunctionBuilder.from_func(example) assert 'test' in fb_example.args assert fb_example.get_arg_names() == ('req', 'test', 'loop') assert fb_example.get_arg_names(only_required=True) == ('req',) @pytest.mark.parametrize('signature,should_match', [('a, *, b', True), ('a,*,b', True), ('a, * , b', True), ('a, *,\nb', True), ('a, *\n,b', True), ('a, b', False), ('a, *args', False), ('a, *args, **kwargs', False), ('*args', False), ('*args, **kwargs', False)]) def test_FunctionBuilder_KWONLY_MARKER(signature, should_match): """ _KWONLY_MARKER matches the keyword-only argument separator, regardless of whitespace. Note: it assumes the signature is valid Python. """ matched = bool(FunctionBuilder._KWONLY_MARKER.search(signature)) message = "{!r}: should_match was {}, but result was {}".format( signature, should_match, matched) assert bool(matched) == should_match, message def test_FunctionBuilder_add_arg_kwonly(): fb = FunctionBuilder('return_val', doc='returns the value', body='return val') broken_func = fb.get_func() with pytest.raises(NameError): broken_func() fb.add_arg('val', default='default_val', kwonly=True) better_func = fb.get_func() assert better_func() == 'default_val' with pytest.raises(ValueError): fb.add_arg('val') assert better_func(val='keyword') == 'keyword' with pytest.raises(TypeError): assert better_func('positional') return @pytest.mark.parametrize( "args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, invocation_str, sig_str", [ ( None, "args", "kwargs", None, "a", dict(a="a"), "*args, a=a, **kwargs", "(*args, a, **kwargs)", ) ], ) def test_get_invocation_sig_str( args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, invocation_str, sig_str, ): fb = FunctionBuilder( name="return_five", body="return 5", args=args, varargs=varargs, varkw=varkw, defaults=defaults, kwonlyargs=kwonlyargs, kwonlydefaults=kwonlydefaults, ) assert fb.get_invocation_str() == invocation_str assert fb.get_sig_str() == sig_str def test_wraps_inner_kwarg_only(): """from https://github.com/mahmoud/boltons/issues/261 mh responds to the issue: You'll notice that when kw-only args are involved the first time (wraps(f)(g)) it works fine. The other way around, however, wraps(g)(f) fails, because by the very nature of funcutils.wraps, you're trying to give f the same signature as g. And f's signature is not like g's. g supports positional b and f() does not. If you want to make a wrapper which converts a keyword-only argument to one that can be positional or keyword only, that'll require a different approach for now. A potential fix would be to pass all function arguments as keywords. But doubt that's the right direction, because, while I have yet to add positional argument only support, that'll definitely throw a wrench into things. """ from boltons.funcutils import wraps def g(a: float, b=10): return a * b def f(a: int, *, b=1): return a * b # all is well here... assert f(3) == 3 assert g(3) == 30 assert wraps(f)(g)(3) == 3 # yay, g got the f default (not so with functools.wraps!) # but this doesn't work with pytest.raises(TypeError): wraps(g)(f)(3) return def test_wraps_async(): # from https://github.com/mahmoud/boltons/issues/194 import asyncio def delayed(func): @wraps(func) async def wrapped(*args, **kw): await asyncio.sleep(1.0) return await func(*args, **kw) return wrapped async def f(): await asyncio.sleep(0.1) assert asyncio.iscoroutinefunction(f) f2 = delayed(f) assert asyncio.iscoroutinefunction(f2) # from https://github.com/mahmoud/boltons/pull/195 def yolo(): def make_time_decorator(wrapped): @wraps(wrapped) async def decorator(*args, **kw): return (await wrapped(*args, **kw)) return decorator return make_time_decorator @yolo() async def foo(x): await asyncio.sleep(x) start_time = time.monotonic() asyncio.run(foo(0.3)) duration = time.monotonic() - start_time # lol windows py37 somehow completes this in under 0.3 # "assert 0.29700000000002547 > 0.3" https://ci.appveyor.com/project/mahmoud/boltons/builds/22261051/job/3jfq1tq2233csqp6 assert duration > 0.25 def test_wraps_hide_wrapped(): new_func = wraps(wrappable_func, injected='b')(lambda a: wrappable_func(a, b=1)) new_sig = inspect.signature(new_func, follow_wrapped=True) assert list(new_sig.parameters.keys()) == ['a', 'b'] new_func = wraps(wrappable_func, injected='b', hide_wrapped=True)(lambda a: wrappable_func(a, b=1)) new_sig = inspect.signature(new_func, follow_wrapped=True) assert list(new_sig.parameters.keys()) == ['a'] new_func = wraps(wrappable_func, injected='b')(lambda a: wrappable_func(a, b=1)) new_new_func = wraps(new_func, injected='a', hide_wrapped=True)(lambda: new_func(a=1)) new_new_sig = inspect.signature(new_new_func, follow_wrapped=True) assert len(new_new_sig.parameters) == 0