test_kwargs_and_defaults.py 9.85 KB
Newer Older
1
# -*- coding: utf-8 -*-
Dean Moldovan's avatar
Dean Moldovan committed
2
import pytest
3
4
5

import env  # noqa: F401

6
from pybind11_tests import kwargs_and_defaults as m
Dean Moldovan's avatar
Dean Moldovan committed
7
8
9


def test_function_signatures(doc):
10
11
    assert doc(m.kw_func0) == "kw_func0(arg0: int, arg1: int) -> str"
    assert doc(m.kw_func1) == "kw_func1(x: int, y: int) -> str"
12
13
14
15
16
    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"
17
    assert doc(m.args_function) == "args_function(*args) -> tuple"
18
19
20
21
22
23
24
25
26
27
28
    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
29
30


31
def test_named_arguments(msg):
32
    assert m.kw_func0(5, 10) == "x=5, y=10"
Dean Moldovan's avatar
Dean Moldovan committed
33

34
35
36
    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"
37

38
39
40
41
42
43
    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
44
45
46

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

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

56
57
    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
58
59


60
def test_arg_and_kwargs():
61
    args = "arg1_value", "arg2_value", 3
62
    assert m.args_function(*args) == args
63

64
65
    args = "a1", "a2"
    kwargs = dict(arg3="a3", arg4=4)
66
    assert m.args_kwargs_function(*args, **kwargs) == (args, kwargs)
67
68
69


def test_mixed_args_and_kwargs(msg):
70
71
72
73
    mpa = m.mixed_plus_args
    mpk = m.mixed_plus_kwargs
    mpak = m.mixed_plus_args_kwargs
    mpakd = m.mixed_plus_args_kwargs_defaults
74
75
76
77
78

    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)
79
80
81
    assert (
        msg(excinfo.value)
        == """
82
83
84
85
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with: 1
86
    """  # noqa: E501 line too long
87
    )
88
89
    with pytest.raises(TypeError) as excinfo:
        assert mpa()
90
91
92
    assert (
        msg(excinfo.value)
        == """
93
94
95
96
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with:
97
    """  # noqa: E501 line too long
98
    )
99

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

        Invoked with: 1; kwargs: i=1
    """  # noqa: E501 line too long
132
    )
133
134
    with pytest.raises(TypeError) as excinfo:
        assert mpakd(1, 2, j=1)
135
136
137
    assert (
        msg(excinfo.value)
        == """
138
        mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported:
139
            1. (i: int = 1, j: float = 3.14159, *args, **kwargs) -> tuple
140
141
142

        Invoked with: 1, 2; kwargs: j=1
    """  # noqa: E501 line too long
143
    )
144
145


146
@pytest.mark.skipif("True")  # ASAN:leak
147
def test_keyword_only_args(msg):
Henry Schreiner's avatar
Henry Schreiner committed
148
149
    assert m.kw_only_all(i=1, j=2) == (1, 2)
    assert m.kw_only_all(j=1, i=2) == (2, 1)
150
151

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
152
        assert m.kw_only_all(i=1) == (1,)
153
154
155
    assert "incompatible function arguments" in str(excinfo.value)

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

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

Henry Schreiner's avatar
Henry Schreiner committed
161
162
163
164
    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)
165

Henry Schreiner's avatar
Henry Schreiner committed
166
167
168
    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)
169

170
171
172
    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})
173
174

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
175
        assert m.kw_only_mixed(i=1) == (1,)
176
177
178
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(RuntimeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
179
        m.register_invalid_kw_only(m)
180
181
182
    assert (
        msg(excinfo.value)
        == """
Henry Schreiner's avatar
Henry Schreiner committed
183
        arg(): cannot specify an unnamed argument after an kw_only() annotation
184
    """
185
    )
186
187


Henry Schreiner's avatar
Henry Schreiner committed
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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)


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__
233
234
235
236
    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
237
238


239
@pytest.mark.xfail("env.PYPY and env.PY2", reason="PyPy2 doesn't double count")
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
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

261
262
263
264
265
266
    assert m.mixed_plus_args_kwargs(5, 6.0, myval, a=myval) == (
        5,
        6.0,
        (myval,),
        {"a": myval},
    )
267
268
    assert refcount(myval) == expected

269
270
271
272
    assert m.args_kwargs_function(7, 8, myval, a=1, b=myval) == (
        (7, 8, myval),
        {"a": 1, "b": myval},
    )
273
274
275
276
277
278
279
280
281
282
283
284
    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)
285
286

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