test_kwargs_and_defaults.py 13.4 KB
Newer Older
Dean Moldovan's avatar
Dean Moldovan committed
1
import pytest
2

3
from pybind11_tests import kwargs_and_defaults as m
Dean Moldovan's avatar
Dean Moldovan committed
4
5
6


def test_function_signatures(doc):
7
8
    assert doc(m.kw_func0) == "kw_func0(arg0: int, arg1: int) -> str"
    assert doc(m.kw_func1) == "kw_func1(x: int, y: int) -> str"
9
10
11
12
13
    assert doc(m.kw_func2) == "kw_func2(x: int = 100, y: int = 200) -> str"
    assert doc(m.kw_func3) == "kw_func3(data: str = 'Hello world!') -> None"
    assert doc(m.kw_func4) == "kw_func4(myList: List[int] = [13, 17]) -> str"
    assert doc(m.kw_func_udl) == "kw_func_udl(x: int, y: int = 300) -> str"
    assert doc(m.kw_func_udl_z) == "kw_func_udl_z(x: int, y: int = 0) -> str"
14
    assert doc(m.args_function) == "args_function(*args) -> tuple"
15
16
17
18
19
20
21
22
23
24
25
    assert (
        doc(m.args_kwargs_function) == "args_kwargs_function(*args, **kwargs) -> tuple"
    )
    assert (
        doc(m.KWClass.foo0)
        == "foo0(self: m.kwargs_and_defaults.KWClass, arg0: int, arg1: float) -> None"
    )
    assert (
        doc(m.KWClass.foo1)
        == "foo1(self: m.kwargs_and_defaults.KWClass, x: int, y: float) -> None"
    )
Dean Moldovan's avatar
Dean Moldovan committed
26
27


28
def test_named_arguments(msg):
29
    assert m.kw_func0(5, 10) == "x=5, y=10"
Dean Moldovan's avatar
Dean Moldovan committed
30

31
32
33
    assert m.kw_func1(5, 10) == "x=5, y=10"
    assert m.kw_func1(5, y=10) == "x=5, y=10"
    assert m.kw_func1(y=10, x=5) == "x=5, y=10"
34

35
36
37
38
39
40
    assert m.kw_func2() == "x=100, y=200"
    assert m.kw_func2(5) == "x=5, y=200"
    assert m.kw_func2(x=5) == "x=5, y=200"
    assert m.kw_func2(y=10) == "x=100, y=10"
    assert m.kw_func2(5, 10) == "x=5, y=10"
    assert m.kw_func2(x=5, y=10) == "x=5, y=10"
Dean Moldovan's avatar
Dean Moldovan committed
41
42
43

    with pytest.raises(TypeError) as excinfo:
        # noinspection PyArgumentList
44
        m.kw_func2(x=5, y=10, z=12)
45
    assert excinfo.match(
46
47
48
        r"(?s)^kw_func2\(\): incompatible.*Invoked with: kwargs: ((x=5|y=10|z=12)(, |$))"
        + "{3}$"
    )
Dean Moldovan's avatar
Dean Moldovan committed
49

50
51
    assert m.kw_func4() == "{13 17}"
    assert m.kw_func4(myList=[1, 2, 3]) == "{1 2 3}"
Dean Moldovan's avatar
Dean Moldovan committed
52

53
54
    assert m.kw_func_udl(x=5, y=10) == "x=5, y=10"
    assert m.kw_func_udl_z(x=5) == "x=5, y=0"
Dean Moldovan's avatar
Dean Moldovan committed
55
56


57
def test_arg_and_kwargs():
58
    args = "arg1_value", "arg2_value", 3
59
    assert m.args_function(*args) == args
60

61
62
    args = "a1", "a2"
    kwargs = dict(arg3="a3", arg4=4)
63
    assert m.args_kwargs_function(*args, **kwargs) == (args, kwargs)
64
65
66


def test_mixed_args_and_kwargs(msg):
67
68
69
70
    mpa = m.mixed_plus_args
    mpk = m.mixed_plus_kwargs
    mpak = m.mixed_plus_args_kwargs
    mpakd = m.mixed_plus_args_kwargs_defaults
71
72
73
74
75

    assert mpa(1, 2.5, 4, 99.5, None) == (1, 2.5, (4, 99.5, None))
    assert mpa(1, 2.5) == (1, 2.5, ())
    with pytest.raises(TypeError) as excinfo:
        assert mpa(1)
76
77
78
    assert (
        msg(excinfo.value)
        == """
79
80
81
82
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with: 1
83
    """
84
    )
85
86
    with pytest.raises(TypeError) as excinfo:
        assert mpa()
87
88
89
    assert (
        msg(excinfo.value)
        == """
90
91
92
93
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with:
94
    """
95
    )
96

97
98
99
100
101
    assert mpk(-2, 3.5, pi=3.14159, e=2.71828) == (
        -2,
        3.5,
        {"e": 2.71828, "pi": 3.14159},
    )
102
    assert mpak(7, 7.7, 7.77, 7.777, 7.7777, minusseven=-7) == (
103
104
105
106
107
        7,
        7.7,
        (7.77, 7.777, 7.7777),
        {"minusseven": -7},
    )
108
109
110
    assert mpakd() == (1, 3.14159, (), {})
    assert mpakd(3) == (3, 3.14159, (), {})
    assert mpakd(j=2.71828) == (1, 2.71828, (), {})
111
    assert mpakd(k=42) == (1, 3.14159, (), {"k": 42})
112
    assert mpakd(1, 1, 2, 3, 5, 8, then=13, followedby=21) == (
113
114
115
116
117
        1,
        1,
        (2, 3, 5, 8),
        {"then": 13, "followedby": 21},
    )
118
    # Arguments specified both positionally and via kwargs should fail:
119
120
    with pytest.raises(TypeError) as excinfo:
        assert mpakd(1, i=1)
121
122
123
    assert (
        msg(excinfo.value)
        == """
124
        mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported:
125
            1. (i: int = 1, j: float = 3.14159, *args, **kwargs) -> tuple
126
127

        Invoked with: 1; kwargs: i=1
128
    """
129
    )
130
131
    with pytest.raises(TypeError) as excinfo:
        assert mpakd(1, 2, j=1)
132
133
134
    assert (
        msg(excinfo.value)
        == """
135
        mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported:
136
            1. (i: int = 1, j: float = 3.14159, *args, **kwargs) -> tuple
137
138

        Invoked with: 1, 2; kwargs: j=1
139
    """
140
    )
141

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    # Arguments after a py::args are automatically keyword-only (pybind 2.9+)
    assert m.args_kwonly(2, 2.5, z=22) == (2, 2.5, (), 22)
    assert m.args_kwonly(2, 2.5, "a", "b", "c", z=22) == (2, 2.5, ("a", "b", "c"), 22)
    assert m.args_kwonly(z=22, i=4, j=16) == (4, 16, (), 22)

    with pytest.raises(TypeError) as excinfo:
        assert m.args_kwonly(2, 2.5, 22)  # missing z= keyword
    assert (
        msg(excinfo.value)
        == """
        args_kwonly(): incompatible function arguments. The following argument types are supported:
            1. (i: int, j: float, *args, z: int) -> tuple

        Invoked with: 2, 2.5, 22
    """
    )

    assert m.args_kwonly_kwargs(i=1, k=4, j=10, z=-1, y=9) == (
        1,
        10,
        (),
        -1,
        {"k": 4, "y": 9},
    )
    assert m.args_kwonly_kwargs(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, z=11, y=12) == (
        1,
        2,
        (3, 4, 5, 6, 7, 8, 9, 10),
        11,
        {"y": 12},
    )
    assert (
        m.args_kwonly_kwargs.__doc__
        == "args_kwonly_kwargs(i: int, j: float, *args, z: int, **kwargs) -> tuple\n"
    )

    assert (
        m.args_kwonly_kwargs_defaults.__doc__
        == "args_kwonly_kwargs_defaults(i: int = 1, j: float = 3.14159, *args, z: int = 42, **kwargs) -> tuple\n"  # noqa: E501 line too long
    )
    assert m.args_kwonly_kwargs_defaults() == (1, 3.14159, (), 42, {})
    assert m.args_kwonly_kwargs_defaults(2) == (2, 3.14159, (), 42, {})
    assert m.args_kwonly_kwargs_defaults(z=-99) == (1, 3.14159, (), -99, {})
    assert m.args_kwonly_kwargs_defaults(5, 6, 7, 8) == (5, 6, (7, 8), 42, {})
    assert m.args_kwonly_kwargs_defaults(5, 6, 7, m=8) == (5, 6, (7,), 42, {"m": 8})
    assert m.args_kwonly_kwargs_defaults(5, 6, 7, m=8, z=9) == (5, 6, (7,), 9, {"m": 8})

189

190
def test_keyword_only_args(msg):
Henry Schreiner's avatar
Henry Schreiner committed
191
192
    assert m.kw_only_all(i=1, j=2) == (1, 2)
    assert m.kw_only_all(j=1, i=2) == (2, 1)
193
194

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
195
        assert m.kw_only_all(i=1) == (1,)
196
197
198
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
199
        assert m.kw_only_all(1, 2) == (1, 2)
200
201
    assert "incompatible function arguments" in str(excinfo.value)

Henry Schreiner's avatar
Henry Schreiner committed
202
    assert m.kw_only_some(1, k=3, j=2) == (1, 2, 3)
203

Henry Schreiner's avatar
Henry Schreiner committed
204
205
206
207
    assert m.kw_only_with_defaults(z=8) == (3, 4, 5, 8)
    assert m.kw_only_with_defaults(2, z=8) == (2, 4, 5, 8)
    assert m.kw_only_with_defaults(2, j=7, k=8, z=9) == (2, 7, 8, 9)
    assert m.kw_only_with_defaults(2, 7, z=9, k=8) == (2, 7, 8, 9)
208

Henry Schreiner's avatar
Henry Schreiner committed
209
210
211
    assert m.kw_only_mixed(1, j=2) == (1, 2)
    assert m.kw_only_mixed(j=2, i=3) == (3, 2)
    assert m.kw_only_mixed(i=2, j=3) == (2, 3)
212

213
214
215
    assert m.kw_only_plus_more(4, 5, k=6, extra=7) == (4, 5, 6, {"extra": 7})
    assert m.kw_only_plus_more(3, k=5, j=4, extra=6) == (3, 4, 5, {"extra": 6})
    assert m.kw_only_plus_more(2, k=3, extra=4) == (2, -1, 3, {"extra": 4})
216
217

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
218
        assert m.kw_only_mixed(i=1) == (1,)
219
220
221
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(RuntimeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
222
        m.register_invalid_kw_only(m)
223
224
225
    assert (
        msg(excinfo.value)
        == """
226
        arg(): cannot specify an unnamed argument after a kw_only() annotation or args() argument
227
    """
228
    )
229

230
231
232
233
234
235
236
237
238
239
240
241
242
    # https://github.com/pybind/pybind11/pull/3402#issuecomment-963341987
    x = m.first_arg_kw_only(i=1)
    x.method()
    x.method(i=1, j=2)
    assert (
        m.first_arg_kw_only.__init__.__doc__
        == "__init__(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: int = 0) -> None\n"  # noqa: E501 line too long
    )
    assert (
        m.first_arg_kw_only.method.__doc__
        == "method(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, *, i: int = 1, j: int = 2) -> None\n"  # noqa: E501 line too long
    )

243

Henry Schreiner's avatar
Henry Schreiner committed
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def test_positional_only_args(msg):
    assert m.pos_only_all(1, 2) == (1, 2)
    assert m.pos_only_all(2, 1) == (2, 1)

    with pytest.raises(TypeError) as excinfo:
        m.pos_only_all(i=1, j=2)
    assert "incompatible function arguments" in str(excinfo.value)

    assert m.pos_only_mix(1, 2) == (1, 2)
    assert m.pos_only_mix(2, j=1) == (2, 1)

    with pytest.raises(TypeError) as excinfo:
        m.pos_only_mix(i=1, j=2)
    assert "incompatible function arguments" in str(excinfo.value)

    assert m.pos_kw_only_mix(1, 2, k=3) == (1, 2, 3)
    assert m.pos_kw_only_mix(1, j=2, k=3) == (1, 2, 3)

    with pytest.raises(TypeError) as excinfo:
        m.pos_kw_only_mix(i=1, j=2, k=3)
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(TypeError) as excinfo:
        m.pos_kw_only_mix(1, 2, 3)
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(TypeError) as excinfo:
        m.pos_only_def_mix()
    assert "incompatible function arguments" in str(excinfo.value)

    assert m.pos_only_def_mix(1) == (1, 2, 3)
    assert m.pos_only_def_mix(1, 4) == (1, 4, 3)
    assert m.pos_only_def_mix(1, 4, 7) == (1, 4, 7)
    assert m.pos_only_def_mix(1, 4, k=7) == (1, 4, 7)

    with pytest.raises(TypeError) as excinfo:
        m.pos_only_def_mix(1, j=4)
    assert "incompatible function arguments" in str(excinfo.value)

283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
    # Mix it with args and kwargs:
    assert (
        m.args_kwonly_full_monty.__doc__
        == "args_kwonly_full_monty(arg0: int = 1, arg1: int = 2, /, j: float = 3.14159, *args, z: int = 42, **kwargs) -> tuple\n"  # noqa: E501 line too long
    )
    assert m.args_kwonly_full_monty() == (1, 2, 3.14159, (), 42, {})
    assert m.args_kwonly_full_monty(8) == (8, 2, 3.14159, (), 42, {})
    assert m.args_kwonly_full_monty(8, 9) == (8, 9, 3.14159, (), 42, {})
    assert m.args_kwonly_full_monty(8, 9, 10) == (8, 9, 10.0, (), 42, {})
    assert m.args_kwonly_full_monty(3, 4, 5, 6, 7, m=8, z=9) == (
        3,
        4,
        5.0,
        (
            6,
            7,
        ),
        9,
        {"m": 8},
    )
    assert m.args_kwonly_full_monty(3, 4, 5, 6, 7, m=8, z=9) == (
        3,
        4,
        5.0,
        (
            6,
            7,
        ),
        9,
        {"m": 8},
    )
    assert m.args_kwonly_full_monty(5, j=7, m=8, z=9) == (5, 2, 7.0, (), 9, {"m": 8})
    assert m.args_kwonly_full_monty(i=5, j=7, m=8, z=9) == (
        1,
        2,
        7.0,
        (),
        9,
        {"i": 5, "m": 8},
    )

324
325
326
327
328
329
330
331
    # pos_only at the beginning of the argument list was "broken" in how it was displayed (though
    # this is fairly useless in practice).  Related to:
    # https://github.com/pybind/pybind11/pull/3402#issuecomment-963341987
    assert (
        m.first_arg_kw_only.pos_only.__doc__
        == "pos_only(self: pybind11_tests.kwargs_and_defaults.first_arg_kw_only, /, i: int, j: int) -> None\n"  # noqa: E501 line too long
    )

Henry Schreiner's avatar
Henry Schreiner committed
332
333
334
335
336
337

def test_signatures():
    assert "kw_only_all(*, i: int, j: int) -> tuple\n" == m.kw_only_all.__doc__
    assert "kw_only_mixed(i: int, *, j: int) -> tuple\n" == m.kw_only_mixed.__doc__
    assert "pos_only_all(i: int, j: int, /) -> tuple\n" == m.pos_only_all.__doc__
    assert "pos_only_mix(i: int, /, j: int) -> tuple\n" == m.pos_only_mix.__doc__
338
339
340
341
    assert (
        "pos_kw_only_mix(i: int, /, j: int, *, k: int) -> tuple\n"
        == m.pos_kw_only_mix.__doc__
    )
Henry Schreiner's avatar
Henry Schreiner committed
342
343


344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def test_args_refcount():
    """Issue/PR #1216 - py::args elements get double-inc_ref()ed when combined with regular
    arguments"""
    refcount = m.arg_refcount_h

    myval = 54321
    expected = refcount(myval)
    assert m.arg_refcount_h(myval) == expected
    assert m.arg_refcount_o(myval) == expected + 1
    assert m.arg_refcount_h(myval) == expected
    assert refcount(myval) == expected

    assert m.mixed_plus_args(1, 2.0, "a", myval) == (1, 2.0, ("a", myval))
    assert refcount(myval) == expected

    assert m.mixed_plus_kwargs(3, 4.0, a=1, b=myval) == (3, 4.0, {"a": 1, "b": myval})
    assert refcount(myval) == expected

    assert m.args_function(-1, myval) == (-1, myval)
    assert refcount(myval) == expected

365
366
367
368
369
370
    assert m.mixed_plus_args_kwargs(5, 6.0, myval, a=myval) == (
        5,
        6.0,
        (myval,),
        {"a": myval},
    )
371
372
    assert refcount(myval) == expected

373
374
375
376
    assert m.args_kwargs_function(7, 8, myval, a=1, b=myval) == (
        (7, 8, myval),
        {"a": 1, "b": myval},
    )
377
378
379
380
381
382
383
384
385
386
387
388
    assert refcount(myval) == expected

    exp3 = refcount(myval, myval, myval)
    assert m.args_refcount(myval, myval, myval) == (exp3, exp3, exp3)
    assert refcount(myval) == expected

    # This function takes the first arg as a `py::object` and the rest as a `py::args`.  Unlike the
    # previous case, when we have both positional and `py::args` we need to construct a new tuple
    # for the `py::args`; in the previous case, we could simply inc_ref and pass on Python's input
    # tuple without having to inc_ref the individual elements, but here we can't, hence the extra
    # refs.
    assert m.mixed_args_refcount(myval, myval, myval) == (exp3 + 3, exp3 + 3, exp3 + 3)
389
390

    assert m.class_default_argument() == "<class 'decimal.Decimal'>"