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