mirror of https://github.com/mahmoud/boltons.git
319 lines
9.0 KiB
Python
319 lines
9.0 KiB
Python
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
|