test_callbacks.py 5.9 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
import time
3
4
5
6
from threading import Thread

import pytest

7
import env  # NOQA: F401
8
from pybind11_tests import callbacks as m
Dean Moldovan's avatar
Dean Moldovan committed
9
10


11
def test_callbacks():
Dean Moldovan's avatar
Dean Moldovan committed
12
13
14
    from functools import partial

    def func1():
15
        return "func1"
Dean Moldovan's avatar
Dean Moldovan committed
16
17

    def func2(a, b, c, d):
18
        return "func2", a, b, c, d
Dean Moldovan's avatar
Dean Moldovan committed
19
20

    def func3(a):
21
22
        return "func3({})".format(a)

23
24
25
26
27
    assert m.test_callback1(func1) == "func1"
    assert m.test_callback2(func2) == ("func2", "Hello", "x", True, 5)
    assert m.test_callback1(partial(func2, 1, 2, 3, 4)) == ("func2", 1, 2, 3, 4)
    assert m.test_callback1(partial(func3, "partial")) == "func3(partial)"
    assert m.test_callback3(lambda i: i + 1) == "func(43) = 44"
Dean Moldovan's avatar
Dean Moldovan committed
28

29
    f = m.test_callback4()
Dean Moldovan's avatar
Dean Moldovan committed
30
    assert f(43) == 44
31
    f = m.test_callback5()
Dean Moldovan's avatar
Dean Moldovan committed
32
33
34
    assert f(number=43) == 44


35
36
37
38
39
40
41
def test_bound_method_callback():
    # Bound Python method:
    class MyClass:
        def double(self, val):
            return 2 * val

    z = MyClass()
42
    assert m.test_callback3(z.double) == "func(43) = 86"
43

44
45
    z = m.CppBoundMethodTest()
    assert m.test_callback3(z.triple) == "func(43) = 129"
46
47


48
49
50
51
def test_keyword_args_and_generalized_unpacking():
    def f(*args, **kwargs):
        return args, kwargs

52
    assert m.test_tuple_unpacking(f) == (("positional", 1, 2, 3, 4, 5, 6), {})
53
54
55
56
    assert m.test_dict_unpacking(f) == (
        ("positional", 1),
        {"key": "value", "a": 1, "b": 2},
    )
57
58
59
    assert m.test_keyword_args(f) == ((), {"x": 10, "y": 20})
    assert m.test_unpacking_and_keywords1(f) == ((1, 2), {"c": 3, "d": 4})
    assert m.test_unpacking_and_keywords2(f) == (
60
        ("positional", 1, 2, 3, 4, 5),
61
        {"key": "value", "a": 1, "b": 2, "c": 3, "d": 4, "e": 5},
62
63
64
    )

    with pytest.raises(TypeError) as excinfo:
65
        m.test_unpacking_error1(f)
66
67
68
    assert "Got multiple values for keyword argument" in str(excinfo.value)

    with pytest.raises(TypeError) as excinfo:
69
        m.test_unpacking_error2(f)
70
71
72
    assert "Got multiple values for keyword argument" in str(excinfo.value)

    with pytest.raises(RuntimeError) as excinfo:
73
        m.test_arg_conversion_error1(f)
74
75
76
    assert "Unable to convert call argument" in str(excinfo.value)

    with pytest.raises(RuntimeError) as excinfo:
77
        m.test_arg_conversion_error2(f)
78
79
80
    assert "Unable to convert call argument" in str(excinfo.value)


Dean Moldovan's avatar
Dean Moldovan committed
81
def test_lambda_closure_cleanup():
82
    m.test_lambda_closure_cleanup()
83
    cstats = m.payload_cstats()
Dean Moldovan's avatar
Dean Moldovan committed
84
85
86
87
88
    assert cstats.alive() == 0
    assert cstats.copy_constructions == 1
    assert cstats.move_constructions >= 1


89
90
91
92
93
def test_cpp_callable_cleanup():
    alive_counts = m.test_cpp_callable_cleanup()
    assert alive_counts == [0, 1, 2, 1, 2, 1, 0]


94
def test_cpp_function_roundtrip():
Dean Moldovan's avatar
Dean Moldovan committed
95
96
    """Test if passing a function pointer from C++ -> Python -> C++ yields the original pointer"""

97
98
99
100
101
102
103
    assert (
        m.test_dummy_function(m.dummy_function) == "matches dummy_function: eval(1) = 2"
    )
    assert (
        m.test_dummy_function(m.roundtrip(m.dummy_function))
        == "matches dummy_function: eval(1) = 2"
    )
104
105
106
107
    assert (
        m.test_dummy_function(m.dummy_function_overloaded)
        == "matches dummy_function: eval(1) = 2"
    )
108
    assert m.roundtrip(None, expect_none=True) is None
109
110
111
112
    assert (
        m.test_dummy_function(lambda x: x + 2)
        == "can't convert to function pointer: eval(1) = 3"
    )
Dean Moldovan's avatar
Dean Moldovan committed
113

114
    with pytest.raises(TypeError) as excinfo:
115
        m.test_dummy_function(m.dummy_function2)
116
    assert "incompatible function arguments" in str(excinfo.value)
Dean Moldovan's avatar
Dean Moldovan committed
117

118
    with pytest.raises(TypeError) as excinfo:
119
        m.test_dummy_function(lambda x, y: x + y)
120
121
122
123
    assert any(
        s in str(excinfo.value)
        for s in ("missing 1 required positional argument", "takes exactly 2 arguments")
    )
Dean Moldovan's avatar
Dean Moldovan committed
124
125
126


def test_function_signatures(doc):
127
128
    assert doc(m.test_callback3) == "test_callback3(arg0: Callable[[int], int]) -> str"
    assert doc(m.test_callback4) == "test_callback4() -> Callable[[int], int]"
129
130
131


def test_movable_object():
132
    assert m.callback_with_movable(lambda _: None) is True
133
134


135
136
137
138
139
140
141
142
143
144
@pytest.mark.skipif(
    "env.PYPY",
    reason="PyPy segfaults on here. See discussion on #1413.",
)
def test_python_builtins():
    """Test if python builtins like sum() can be used as callbacks"""
    assert m.test_sum_builtin(sum, [1, 2, 3]) == 6
    assert m.test_sum_builtin(sum, []) == 0


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def test_async_callbacks():
    # serves as state for async callback
    class Item:
        def __init__(self, value):
            self.value = value

    res = []

    # generate stateful lambda that will store result in `res`
    def gen_f():
        s = Item(3)
        return lambda j: res.append(s.value + j)

    # do some work async
    work = [1, 2, 3, 4]
    m.test_async_callback(gen_f(), work)
    # wait until work is done
    from time import sleep
163

164
    sleep(0.5)
165
    assert sum(res) == sum(x + 3 for x in work)
166
167
168
169
170
171


def test_async_async_callbacks():
    t = Thread(target=test_async_callbacks)
    t.start()
    t.join()
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202


def test_callback_num_times():
    # Super-simple micro-benchmarking related to PR #2919.
    # Example runtimes (Intel Xeon 2.2GHz, fully optimized):
    #   num_millions  1, repeats  2:  0.1 secs
    #   num_millions 20, repeats 10: 11.5 secs
    one_million = 1000000
    num_millions = 1  # Try 20 for actual micro-benchmarking.
    repeats = 2  # Try 10.
    rates = []
    for rep in range(repeats):
        t0 = time.time()
        m.callback_num_times(lambda: None, num_millions * one_million)
        td = time.time() - t0
        rate = num_millions / td if td else 0
        rates.append(rate)
        if not rep:
            print()
        print(
            "callback_num_times: {:d} million / {:.3f} seconds = {:.3f} million / second".format(
                num_millions, td, rate
            )
        )
    if len(rates) > 1:
        print("Min    Mean   Max")
        print(
            "{:6.3f} {:6.3f} {:6.3f}".format(
                min(rates), sum(rates) / len(rates), max(rates)
            )
        )