• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2import pytest
3import re
4
5import env  # noqa: F401
6
7from pybind11_tests import factory_constructors as m
8from pybind11_tests.factory_constructors import tag
9from pybind11_tests import ConstructorStats
10
11
12def test_init_factory_basic():
13    """Tests py::init_factory() wrapper around various ways of returning the object"""
14
15    cstats = [
16        ConstructorStats.get(c)
17        for c in [m.TestFactory1, m.TestFactory2, m.TestFactory3]
18    ]
19    cstats[0].alive()  # force gc
20    n_inst = ConstructorStats.detail_reg_inst()
21
22    x1 = m.TestFactory1(tag.unique_ptr, 3)
23    assert x1.value == "3"
24    y1 = m.TestFactory1(tag.pointer)
25    assert y1.value == "(empty)"
26    z1 = m.TestFactory1("hi!")
27    assert z1.value == "hi!"
28
29    assert ConstructorStats.detail_reg_inst() == n_inst + 3
30
31    x2 = m.TestFactory2(tag.move)
32    assert x2.value == "(empty2)"
33    y2 = m.TestFactory2(tag.pointer, 7)
34    assert y2.value == "7"
35    z2 = m.TestFactory2(tag.unique_ptr, "hi again")
36    assert z2.value == "hi again"
37
38    assert ConstructorStats.detail_reg_inst() == n_inst + 6
39
40    x3 = m.TestFactory3(tag.shared_ptr)
41    assert x3.value == "(empty3)"
42    y3 = m.TestFactory3(tag.pointer, 42)
43    assert y3.value == "42"
44    z3 = m.TestFactory3("bye")
45    assert z3.value == "bye"
46
47    for null_ptr_kind in [tag.null_ptr, tag.null_unique_ptr, tag.null_shared_ptr]:
48        with pytest.raises(TypeError) as excinfo:
49            m.TestFactory3(null_ptr_kind)
50        assert (
51            str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
52        )
53
54    assert [i.alive() for i in cstats] == [3, 3, 3]
55    assert ConstructorStats.detail_reg_inst() == n_inst + 9
56
57    del x1, y2, y3, z3
58    assert [i.alive() for i in cstats] == [2, 2, 1]
59    assert ConstructorStats.detail_reg_inst() == n_inst + 5
60    del x2, x3, y1, z1, z2
61    assert [i.alive() for i in cstats] == [0, 0, 0]
62    assert ConstructorStats.detail_reg_inst() == n_inst
63
64    assert [i.values() for i in cstats] == [
65        ["3", "hi!"],
66        ["7", "hi again"],
67        ["42", "bye"],
68    ]
69    assert [i.default_constructions for i in cstats] == [1, 1, 1]
70
71
72def test_init_factory_signature(msg):
73    with pytest.raises(TypeError) as excinfo:
74        m.TestFactory1("invalid", "constructor", "arguments")
75    assert (
76        msg(excinfo.value)
77        == """
78        __init__(): incompatible constructor arguments. The following argument types are supported:
79            1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int)
80            2. m.factory_constructors.TestFactory1(arg0: str)
81            3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag)
82            4. m.factory_constructors.TestFactory1(arg0: handle, arg1: int, arg2: handle)
83
84        Invoked with: 'invalid', 'constructor', 'arguments'
85    """  # noqa: E501 line too long
86    )
87
88    assert (
89        msg(m.TestFactory1.__init__.__doc__)
90        == """
91        __init__(*args, **kwargs)
92        Overloaded function.
93
94        1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int) -> None
95
96        2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None
97
98        3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None
99
100        4. __init__(self: m.factory_constructors.TestFactory1, arg0: handle, arg1: int, arg2: handle) -> None
101    """  # noqa: E501 line too long
102    )
103
104
105def test_init_factory_casting():
106    """Tests py::init_factory() wrapper with various upcasting and downcasting returns"""
107
108    cstats = [
109        ConstructorStats.get(c)
110        for c in [m.TestFactory3, m.TestFactory4, m.TestFactory5]
111    ]
112    cstats[0].alive()  # force gc
113    n_inst = ConstructorStats.detail_reg_inst()
114
115    # Construction from derived references:
116    a = m.TestFactory3(tag.pointer, tag.TF4, 4)
117    assert a.value == "4"
118    b = m.TestFactory3(tag.shared_ptr, tag.TF4, 5)
119    assert b.value == "5"
120    c = m.TestFactory3(tag.pointer, tag.TF5, 6)
121    assert c.value == "6"
122    d = m.TestFactory3(tag.shared_ptr, tag.TF5, 7)
123    assert d.value == "7"
124
125    assert ConstructorStats.detail_reg_inst() == n_inst + 4
126
127    # Shared a lambda with TF3:
128    e = m.TestFactory4(tag.pointer, tag.TF4, 8)
129    assert e.value == "8"
130
131    assert ConstructorStats.detail_reg_inst() == n_inst + 5
132    assert [i.alive() for i in cstats] == [5, 3, 2]
133
134    del a
135    assert [i.alive() for i in cstats] == [4, 2, 2]
136    assert ConstructorStats.detail_reg_inst() == n_inst + 4
137
138    del b, c, e
139    assert [i.alive() for i in cstats] == [1, 0, 1]
140    assert ConstructorStats.detail_reg_inst() == n_inst + 1
141
142    del d
143    assert [i.alive() for i in cstats] == [0, 0, 0]
144    assert ConstructorStats.detail_reg_inst() == n_inst
145
146    assert [i.values() for i in cstats] == [
147        ["4", "5", "6", "7", "8"],
148        ["4", "5", "8"],
149        ["6", "7"],
150    ]
151
152
153def test_init_factory_alias():
154    """Tests py::init_factory() wrapper with value conversions and alias types"""
155
156    cstats = [m.TestFactory6.get_cstats(), m.TestFactory6.get_alias_cstats()]
157    cstats[0].alive()  # force gc
158    n_inst = ConstructorStats.detail_reg_inst()
159
160    a = m.TestFactory6(tag.base, 1)
161    assert a.get() == 1
162    assert not a.has_alias()
163    b = m.TestFactory6(tag.alias, "hi there")
164    assert b.get() == 8
165    assert b.has_alias()
166    c = m.TestFactory6(tag.alias, 3)
167    assert c.get() == 3
168    assert c.has_alias()
169    d = m.TestFactory6(tag.alias, tag.pointer, 4)
170    assert d.get() == 4
171    assert d.has_alias()
172    e = m.TestFactory6(tag.base, tag.pointer, 5)
173    assert e.get() == 5
174    assert not e.has_alias()
175    f = m.TestFactory6(tag.base, tag.alias, tag.pointer, 6)
176    assert f.get() == 6
177    assert f.has_alias()
178
179    assert ConstructorStats.detail_reg_inst() == n_inst + 6
180    assert [i.alive() for i in cstats] == [6, 4]
181
182    del a, b, e
183    assert [i.alive() for i in cstats] == [3, 3]
184    assert ConstructorStats.detail_reg_inst() == n_inst + 3
185    del f, c, d
186    assert [i.alive() for i in cstats] == [0, 0]
187    assert ConstructorStats.detail_reg_inst() == n_inst
188
189    class MyTest(m.TestFactory6):
190        def __init__(self, *args):
191            m.TestFactory6.__init__(self, *args)
192
193        def get(self):
194            return -5 + m.TestFactory6.get(self)
195
196    # Return Class by value, moved into new alias:
197    z = MyTest(tag.base, 123)
198    assert z.get() == 118
199    assert z.has_alias()
200
201    # Return alias by value, moved into new alias:
202    y = MyTest(tag.alias, "why hello!")
203    assert y.get() == 5
204    assert y.has_alias()
205
206    # Return Class by pointer, moved into new alias then original destroyed:
207    x = MyTest(tag.base, tag.pointer, 47)
208    assert x.get() == 42
209    assert x.has_alias()
210
211    assert ConstructorStats.detail_reg_inst() == n_inst + 3
212    assert [i.alive() for i in cstats] == [3, 3]
213    del x, y, z
214    assert [i.alive() for i in cstats] == [0, 0]
215    assert ConstructorStats.detail_reg_inst() == n_inst
216
217    assert [i.values() for i in cstats] == [
218        ["1", "8", "3", "4", "5", "6", "123", "10", "47"],
219        ["hi there", "3", "4", "6", "move", "123", "why hello!", "move", "47"],
220    ]
221
222
223def test_init_factory_dual():
224    """Tests init factory functions with dual main/alias factory functions"""
225    from pybind11_tests.factory_constructors import TestFactory7
226
227    cstats = [TestFactory7.get_cstats(), TestFactory7.get_alias_cstats()]
228    cstats[0].alive()  # force gc
229    n_inst = ConstructorStats.detail_reg_inst()
230
231    class PythFactory7(TestFactory7):
232        def get(self):
233            return 100 + TestFactory7.get(self)
234
235    a1 = TestFactory7(1)
236    a2 = PythFactory7(2)
237    assert a1.get() == 1
238    assert a2.get() == 102
239    assert not a1.has_alias()
240    assert a2.has_alias()
241
242    b1 = TestFactory7(tag.pointer, 3)
243    b2 = PythFactory7(tag.pointer, 4)
244    assert b1.get() == 3
245    assert b2.get() == 104
246    assert not b1.has_alias()
247    assert b2.has_alias()
248
249    c1 = TestFactory7(tag.mixed, 5)
250    c2 = PythFactory7(tag.mixed, 6)
251    assert c1.get() == 5
252    assert c2.get() == 106
253    assert not c1.has_alias()
254    assert c2.has_alias()
255
256    d1 = TestFactory7(tag.base, tag.pointer, 7)
257    d2 = PythFactory7(tag.base, tag.pointer, 8)
258    assert d1.get() == 7
259    assert d2.get() == 108
260    assert not d1.has_alias()
261    assert d2.has_alias()
262
263    # Both return an alias; the second multiplies the value by 10:
264    e1 = TestFactory7(tag.alias, tag.pointer, 9)
265    e2 = PythFactory7(tag.alias, tag.pointer, 10)
266    assert e1.get() == 9
267    assert e2.get() == 200
268    assert e1.has_alias()
269    assert e2.has_alias()
270
271    f1 = TestFactory7(tag.shared_ptr, tag.base, 11)
272    f2 = PythFactory7(tag.shared_ptr, tag.base, 12)
273    assert f1.get() == 11
274    assert f2.get() == 112
275    assert not f1.has_alias()
276    assert f2.has_alias()
277
278    g1 = TestFactory7(tag.shared_ptr, tag.invalid_base, 13)
279    assert g1.get() == 13
280    assert not g1.has_alias()
281    with pytest.raises(TypeError) as excinfo:
282        PythFactory7(tag.shared_ptr, tag.invalid_base, 14)
283    assert (
284        str(excinfo.value)
285        == "pybind11::init(): construction failed: returned holder-wrapped instance is not an "
286        "alias instance"
287    )
288
289    assert [i.alive() for i in cstats] == [13, 7]
290    assert ConstructorStats.detail_reg_inst() == n_inst + 13
291
292    del a1, a2, b1, d1, e1, e2
293    assert [i.alive() for i in cstats] == [7, 4]
294    assert ConstructorStats.detail_reg_inst() == n_inst + 7
295    del b2, c1, c2, d2, f1, f2, g1
296    assert [i.alive() for i in cstats] == [0, 0]
297    assert ConstructorStats.detail_reg_inst() == n_inst
298
299    assert [i.values() for i in cstats] == [
300        ["1", "2", "3", "4", "5", "6", "7", "8", "9", "100", "11", "12", "13", "14"],
301        ["2", "4", "6", "8", "9", "100", "12"],
302    ]
303
304
305def test_no_placement_new(capture):
306    """Prior to 2.2, `py::init<...>` relied on the type supporting placement
307    new; this tests a class without placement new support."""
308    with capture:
309        a = m.NoPlacementNew(123)
310
311    found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
312    assert found
313    assert a.i == 123
314    with capture:
315        del a
316        pytest.gc_collect()
317    assert capture == "operator delete called on " + found.group(1)
318
319    with capture:
320        b = m.NoPlacementNew()
321
322    found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
323    assert found
324    assert b.i == 100
325    with capture:
326        del b
327        pytest.gc_collect()
328    assert capture == "operator delete called on " + found.group(1)
329
330
331def test_multiple_inheritance():
332    class MITest(m.TestFactory1, m.TestFactory2):
333        def __init__(self):
334            m.TestFactory1.__init__(self, tag.unique_ptr, 33)
335            m.TestFactory2.__init__(self, tag.move)
336
337    a = MITest()
338    assert m.TestFactory1.value.fget(a) == "33"
339    assert m.TestFactory2.value.fget(a) == "(empty2)"
340
341
342def create_and_destroy(*args):
343    a = m.NoisyAlloc(*args)
344    print("---")
345    del a
346    pytest.gc_collect()
347
348
349def strip_comments(s):
350    return re.sub(r"\s+#.*", "", s)
351
352
353def test_reallocation_a(capture, msg):
354    """When the constructor is overloaded, previous overloads can require a preallocated value.
355    This test makes sure that such preallocated values only happen when they might be necessary,
356    and that they are deallocated properly."""
357
358    pytest.gc_collect()
359
360    with capture:
361        create_and_destroy(1)
362    assert (
363        msg(capture)
364        == """
365        noisy new
366        noisy placement new
367        NoisyAlloc(int 1)
368        ---
369        ~NoisyAlloc()
370        noisy delete
371    """
372    )
373
374
375def test_reallocation_b(capture, msg):
376    with capture:
377        create_and_destroy(1.5)
378    assert msg(capture) == strip_comments(
379        """
380        noisy new               # allocation required to attempt first overload
381        noisy delete            # have to dealloc before considering factory init overload
382        noisy new               # pointer factory calling "new", part 1: allocation
383        NoisyAlloc(double 1.5)  # ... part two, invoking constructor
384        ---
385        ~NoisyAlloc()  # Destructor
386        noisy delete   # operator delete
387    """
388    )
389
390
391def test_reallocation_c(capture, msg):
392    with capture:
393        create_and_destroy(2, 3)
394    assert msg(capture) == strip_comments(
395        """
396        noisy new          # pointer factory calling "new", allocation
397        NoisyAlloc(int 2)  # constructor
398        ---
399        ~NoisyAlloc()  # Destructor
400        noisy delete   # operator delete
401    """
402    )
403
404
405def test_reallocation_d(capture, msg):
406    with capture:
407        create_and_destroy(2.5, 3)
408    assert msg(capture) == strip_comments(
409        """
410        NoisyAlloc(double 2.5)  # construction (local func variable: operator_new not called)
411        noisy new               # return-by-value "new" part 1: allocation
412        ~NoisyAlloc()           # moved-away local func variable destruction
413        ---
414        ~NoisyAlloc()  # Destructor
415        noisy delete   # operator delete
416    """
417    )
418
419
420def test_reallocation_e(capture, msg):
421    with capture:
422        create_and_destroy(3.5, 4.5)
423    assert msg(capture) == strip_comments(
424        """
425        noisy new               # preallocation needed before invoking placement-new overload
426        noisy placement new     # Placement new
427        NoisyAlloc(double 3.5)  # construction
428        ---
429        ~NoisyAlloc()  # Destructor
430        noisy delete   # operator delete
431    """
432    )
433
434
435def test_reallocation_f(capture, msg):
436    with capture:
437        create_and_destroy(4, 0.5)
438    assert msg(capture) == strip_comments(
439        """
440        noisy new          # preallocation needed before invoking placement-new overload
441        noisy delete       # deallocation of preallocated storage
442        noisy new          # Factory pointer allocation
443        NoisyAlloc(int 4)  # factory pointer construction
444        ---
445        ~NoisyAlloc()  # Destructor
446        noisy delete   # operator delete
447    """
448    )
449
450
451def test_reallocation_g(capture, msg):
452    with capture:
453        create_and_destroy(5, "hi")
454    assert msg(capture) == strip_comments(
455        """
456        noisy new            # preallocation needed before invoking first placement new
457        noisy delete         # delete before considering new-style constructor
458        noisy new            # preallocation for second placement new
459        noisy placement new  # Placement new in the second placement new overload
460        NoisyAlloc(int 5)    # construction
461        ---
462        ~NoisyAlloc()  # Destructor
463        noisy delete   # operator delete
464    """
465    )
466
467
468@pytest.mark.skipif("env.PY2")
469def test_invalid_self():
470    """Tests invocation of the pybind-registered base class with an invalid `self` argument.  You
471    can only actually do this on Python 3: Python 2 raises an exception itself if you try."""
472
473    class NotPybindDerived(object):
474        pass
475
476    # Attempts to initialize with an invalid type passed as `self`:
477    class BrokenTF1(m.TestFactory1):
478        def __init__(self, bad):
479            if bad == 1:
480                a = m.TestFactory2(tag.pointer, 1)
481                m.TestFactory1.__init__(a, tag.pointer)
482            elif bad == 2:
483                a = NotPybindDerived()
484                m.TestFactory1.__init__(a, tag.pointer)
485
486    # Same as above, but for a class with an alias:
487    class BrokenTF6(m.TestFactory6):
488        def __init__(self, bad):
489            if bad == 1:
490                a = m.TestFactory2(tag.pointer, 1)
491                m.TestFactory6.__init__(a, tag.base, 1)
492            elif bad == 2:
493                a = m.TestFactory2(tag.pointer, 1)
494                m.TestFactory6.__init__(a, tag.alias, 1)
495            elif bad == 3:
496                m.TestFactory6.__init__(
497                    NotPybindDerived.__new__(NotPybindDerived), tag.base, 1
498                )
499            elif bad == 4:
500                m.TestFactory6.__init__(
501                    NotPybindDerived.__new__(NotPybindDerived), tag.alias, 1
502                )
503
504    for arg in (1, 2):
505        with pytest.raises(TypeError) as excinfo:
506            BrokenTF1(arg)
507        assert (
508            str(excinfo.value)
509            == "__init__(self, ...) called with invalid `self` argument"
510        )
511
512    for arg in (1, 2, 3, 4):
513        with pytest.raises(TypeError) as excinfo:
514            BrokenTF6(arg)
515        assert (
516            str(excinfo.value)
517            == "__init__(self, ...) called with invalid `self` argument"
518        )
519