test_kwargs_and_defaults.py 9.48 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
18
19
20
21
22
    assert doc(m.args_function) == "args_function(*args) -> tuple"
    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
23
24


25
def test_named_arguments(msg):
26
    assert m.kw_func0(5, 10) == "x=5, y=10"
Dean Moldovan's avatar
Dean Moldovan committed
27

28
29
30
    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"
31

32
33
34
35
36
37
    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
38
39
40

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

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

48
49
    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
50
51


52
53
def test_arg_and_kwargs():
    args = 'arg1_value', 'arg2_value', 3
54
    assert m.args_function(*args) == args
55
56
57

    args = 'a1', 'a2'
    kwargs = dict(arg3='a3', arg4=4)
58
    assert m.args_kwargs_function(*args, **kwargs) == (args, kwargs)
59
60
61


def test_mixed_args_and_kwargs(msg):
62
63
64
65
    mpa = m.mixed_plus_args
    mpk = m.mixed_plus_kwargs
    mpak = m.mixed_plus_args_kwargs
    mpakd = m.mixed_plus_args_kwargs_defaults
66
67
68
69
70
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)
    assert msg(excinfo.value) == """
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with: 1
76
    """  # noqa: E501 line too long
77
78
79
80
81
82
83
    with pytest.raises(TypeError) as excinfo:
        assert mpa()
    assert msg(excinfo.value) == """
        mixed_plus_args(): incompatible function arguments. The following argument types are supported:
            1. (arg0: int, arg1: float, *args) -> tuple

        Invoked with:
84
    """  # noqa: E501 line too long
85
86
87
88
89
90
91
92
93
94

    assert mpk(-2, 3.5, pi=3.14159, e=2.71828) == (-2, 3.5, {'e': 2.71828, 'pi': 3.14159})
    assert mpak(7, 7.7, 7.77, 7.777, 7.7777, minusseven=-7) == (
        7, 7.7, (7.77, 7.777, 7.7777), {'minusseven': -7})
    assert mpakd() == (1, 3.14159, (), {})
    assert mpakd(3) == (3, 3.14159, (), {})
    assert mpakd(j=2.71828) == (1, 2.71828, (), {})
    assert mpakd(k=42) == (1, 3.14159, (), {'k': 42})
    assert mpakd(1, 1, 2, 3, 5, 8, then=13, followedby=21) == (
        1, 1, (2, 3, 5, 8), {'then': 13, 'followedby': 21})
95
    # Arguments specified both positionally and via kwargs should fail:
96
97
98
    with pytest.raises(TypeError) as excinfo:
        assert mpakd(1, i=1)
    assert msg(excinfo.value) == """
99
        mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported:
100
            1. (i: int = 1, j: float = 3.14159, *args, **kwargs) -> tuple
101
102
103

        Invoked with: 1; kwargs: i=1
    """  # noqa: E501 line too long
104
105
106
    with pytest.raises(TypeError) as excinfo:
        assert mpakd(1, 2, j=1)
    assert msg(excinfo.value) == """
107
        mixed_plus_args_kwargs_defaults(): incompatible function arguments. The following argument types are supported:
108
            1. (i: int = 1, j: float = 3.14159, *args, **kwargs) -> tuple
109
110
111

        Invoked with: 1, 2; kwargs: j=1
    """  # noqa: E501 line too long
112
113


114
def test_keyword_only_args(msg):
Henry Schreiner's avatar
Henry Schreiner committed
115
116
    assert m.kw_only_all(i=1, j=2) == (1, 2)
    assert m.kw_only_all(j=1, i=2) == (2, 1)
117
118

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
119
        assert m.kw_only_all(i=1) == (1,)
120
121
122
    assert "incompatible function arguments" in str(excinfo.value)

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

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

Henry Schreiner's avatar
Henry Schreiner committed
128
129
130
131
    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)
132

Henry Schreiner's avatar
Henry Schreiner committed
133
134
135
    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)
136

Henry Schreiner's avatar
Henry Schreiner committed
137
138
139
    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})
140
141

    with pytest.raises(TypeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
142
        assert m.kw_only_mixed(i=1) == (1,)
143
144
145
    assert "incompatible function arguments" in str(excinfo.value)

    with pytest.raises(RuntimeError) as excinfo:
Henry Schreiner's avatar
Henry Schreiner committed
146
        m.register_invalid_kw_only(m)
147
    assert msg(excinfo.value) == """
Henry Schreiner's avatar
Henry Schreiner committed
148
        arg(): cannot specify an unnamed argument after an kw_only() annotation
149
150
151
    """


Henry Schreiner's avatar
Henry Schreiner committed
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
189
190
191
192
193
194
195
196
197
198
199
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__
    assert "pos_kw_only_mix(i: int, /, j: int, *, k: int) -> tuple\n" == m.pos_kw_only_mix.__doc__


200
@pytest.mark.xfail("env.PYPY and env.PY2", reason="PyPy2 doesn't double count")
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
233
234
235
236
237
238
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

    assert m.mixed_plus_args_kwargs(5, 6.0, myval, a=myval) == (5, 6.0, (myval,), {"a": myval})
    assert refcount(myval) == expected

    assert m.args_kwargs_function(7, 8, myval, a=1, b=myval) == \
        ((7, 8, myval), {"a": 1, "b": myval})
    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)
239
240

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