517 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			517 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
| import re
 | |
| 
 | |
| import pytest
 | |
| 
 | |
| from pybind11_tests import ConstructorStats
 | |
| from pybind11_tests import factory_constructors as m
 | |
| from pybind11_tests.factory_constructors import tag
 | |
| 
 | |
| 
 | |
| def test_init_factory_basic():
 | |
|     """Tests py::init_factory() wrapper around various ways of returning the object"""
 | |
| 
 | |
|     cstats = [
 | |
|         ConstructorStats.get(c)
 | |
|         for c in [m.TestFactory1, m.TestFactory2, m.TestFactory3]
 | |
|     ]
 | |
|     cstats[0].alive()  # force gc
 | |
|     n_inst = ConstructorStats.detail_reg_inst()
 | |
| 
 | |
|     x1 = m.TestFactory1(tag.unique_ptr, 3)
 | |
|     assert x1.value == "3"
 | |
|     y1 = m.TestFactory1(tag.pointer)
 | |
|     assert y1.value == "(empty)"
 | |
|     z1 = m.TestFactory1("hi!")
 | |
|     assert z1.value == "hi!"
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 3
 | |
| 
 | |
|     x2 = m.TestFactory2(tag.move)
 | |
|     assert x2.value == "(empty2)"
 | |
|     y2 = m.TestFactory2(tag.pointer, 7)
 | |
|     assert y2.value == "7"
 | |
|     z2 = m.TestFactory2(tag.unique_ptr, "hi again")
 | |
|     assert z2.value == "hi again"
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 6
 | |
| 
 | |
|     x3 = m.TestFactory3(tag.shared_ptr)
 | |
|     assert x3.value == "(empty3)"
 | |
|     y3 = m.TestFactory3(tag.pointer, 42)
 | |
|     assert y3.value == "42"
 | |
|     z3 = m.TestFactory3("bye")
 | |
|     assert z3.value == "bye"
 | |
| 
 | |
|     for null_ptr_kind in [tag.null_ptr, tag.null_unique_ptr, tag.null_shared_ptr]:
 | |
|         with pytest.raises(TypeError) as excinfo:
 | |
|             m.TestFactory3(null_ptr_kind)
 | |
|         assert (
 | |
|             str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
 | |
|         )
 | |
| 
 | |
|     assert [i.alive() for i in cstats] == [3, 3, 3]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 9
 | |
| 
 | |
|     del x1, y2, y3, z3
 | |
|     assert [i.alive() for i in cstats] == [2, 2, 1]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 5
 | |
|     del x2, x3, y1, z1, z2
 | |
|     assert [i.alive() for i in cstats] == [0, 0, 0]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst
 | |
| 
 | |
|     assert [i.values() for i in cstats] == [
 | |
|         ["3", "hi!"],
 | |
|         ["7", "hi again"],
 | |
|         ["42", "bye"],
 | |
|     ]
 | |
|     assert [i.default_constructions for i in cstats] == [1, 1, 1]
 | |
| 
 | |
| 
 | |
| def test_init_factory_signature(msg):
 | |
|     with pytest.raises(TypeError) as excinfo:
 | |
|         m.TestFactory1("invalid", "constructor", "arguments")
 | |
|     assert (
 | |
|         msg(excinfo.value)
 | |
|         == """
 | |
|         __init__(): incompatible constructor arguments. The following argument types are supported:
 | |
|             1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int)
 | |
|             2. m.factory_constructors.TestFactory1(arg0: str)
 | |
|             3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag)
 | |
|             4. m.factory_constructors.TestFactory1(arg0: handle, arg1: int, arg2: handle)
 | |
| 
 | |
|         Invoked with: 'invalid', 'constructor', 'arguments'
 | |
|     """
 | |
|     )
 | |
| 
 | |
|     assert (
 | |
|         msg(m.TestFactory1.__init__.__doc__)
 | |
|         == """
 | |
|         __init__(*args, **kwargs)
 | |
|         Overloaded function.
 | |
| 
 | |
|         1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int) -> None
 | |
| 
 | |
|         2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None
 | |
| 
 | |
|         3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None
 | |
| 
 | |
|         4. __init__(self: m.factory_constructors.TestFactory1, arg0: handle, arg1: int, arg2: handle) -> None
 | |
|     """  # noqa: E501 line too long
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_init_factory_casting():
 | |
|     """Tests py::init_factory() wrapper with various upcasting and downcasting returns"""
 | |
| 
 | |
|     cstats = [
 | |
|         ConstructorStats.get(c)
 | |
|         for c in [m.TestFactory3, m.TestFactory4, m.TestFactory5]
 | |
|     ]
 | |
|     cstats[0].alive()  # force gc
 | |
|     n_inst = ConstructorStats.detail_reg_inst()
 | |
| 
 | |
|     # Construction from derived references:
 | |
|     a = m.TestFactory3(tag.pointer, tag.TF4, 4)
 | |
|     assert a.value == "4"
 | |
|     b = m.TestFactory3(tag.shared_ptr, tag.TF4, 5)
 | |
|     assert b.value == "5"
 | |
|     c = m.TestFactory3(tag.pointer, tag.TF5, 6)
 | |
|     assert c.value == "6"
 | |
|     d = m.TestFactory3(tag.shared_ptr, tag.TF5, 7)
 | |
|     assert d.value == "7"
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 4
 | |
| 
 | |
|     # Shared a lambda with TF3:
 | |
|     e = m.TestFactory4(tag.pointer, tag.TF4, 8)
 | |
|     assert e.value == "8"
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 5
 | |
|     assert [i.alive() for i in cstats] == [5, 3, 2]
 | |
| 
 | |
|     del a
 | |
|     assert [i.alive() for i in cstats] == [4, 2, 2]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 4
 | |
| 
 | |
|     del b, c, e
 | |
|     assert [i.alive() for i in cstats] == [1, 0, 1]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 1
 | |
| 
 | |
|     del d
 | |
|     assert [i.alive() for i in cstats] == [0, 0, 0]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst
 | |
| 
 | |
|     assert [i.values() for i in cstats] == [
 | |
|         ["4", "5", "6", "7", "8"],
 | |
|         ["4", "5", "8"],
 | |
|         ["6", "7"],
 | |
|     ]
 | |
| 
 | |
| 
 | |
| def test_init_factory_alias():
 | |
|     """Tests py::init_factory() wrapper with value conversions and alias types"""
 | |
| 
 | |
|     cstats = [m.TestFactory6.get_cstats(), m.TestFactory6.get_alias_cstats()]
 | |
|     cstats[0].alive()  # force gc
 | |
|     n_inst = ConstructorStats.detail_reg_inst()
 | |
| 
 | |
|     a = m.TestFactory6(tag.base, 1)
 | |
|     assert a.get() == 1
 | |
|     assert not a.has_alias()
 | |
|     b = m.TestFactory6(tag.alias, "hi there")
 | |
|     assert b.get() == 8
 | |
|     assert b.has_alias()
 | |
|     c = m.TestFactory6(tag.alias, 3)
 | |
|     assert c.get() == 3
 | |
|     assert c.has_alias()
 | |
|     d = m.TestFactory6(tag.alias, tag.pointer, 4)
 | |
|     assert d.get() == 4
 | |
|     assert d.has_alias()
 | |
|     e = m.TestFactory6(tag.base, tag.pointer, 5)
 | |
|     assert e.get() == 5
 | |
|     assert not e.has_alias()
 | |
|     f = m.TestFactory6(tag.base, tag.alias, tag.pointer, 6)
 | |
|     assert f.get() == 6
 | |
|     assert f.has_alias()
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 6
 | |
|     assert [i.alive() for i in cstats] == [6, 4]
 | |
| 
 | |
|     del a, b, e
 | |
|     assert [i.alive() for i in cstats] == [3, 3]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 3
 | |
|     del f, c, d
 | |
|     assert [i.alive() for i in cstats] == [0, 0]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst
 | |
| 
 | |
|     class MyTest(m.TestFactory6):
 | |
|         def __init__(self, *args):
 | |
|             m.TestFactory6.__init__(self, *args)
 | |
| 
 | |
|         def get(self):
 | |
|             return -5 + m.TestFactory6.get(self)
 | |
| 
 | |
|     # Return Class by value, moved into new alias:
 | |
|     z = MyTest(tag.base, 123)
 | |
|     assert z.get() == 118
 | |
|     assert z.has_alias()
 | |
| 
 | |
|     # Return alias by value, moved into new alias:
 | |
|     y = MyTest(tag.alias, "why hello!")
 | |
|     assert y.get() == 5
 | |
|     assert y.has_alias()
 | |
| 
 | |
|     # Return Class by pointer, moved into new alias then original destroyed:
 | |
|     x = MyTest(tag.base, tag.pointer, 47)
 | |
|     assert x.get() == 42
 | |
|     assert x.has_alias()
 | |
| 
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 3
 | |
|     assert [i.alive() for i in cstats] == [3, 3]
 | |
|     del x, y, z
 | |
|     assert [i.alive() for i in cstats] == [0, 0]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst
 | |
| 
 | |
|     assert [i.values() for i in cstats] == [
 | |
|         ["1", "8", "3", "4", "5", "6", "123", "10", "47"],
 | |
|         ["hi there", "3", "4", "6", "move", "123", "why hello!", "move", "47"],
 | |
|     ]
 | |
| 
 | |
| 
 | |
| def test_init_factory_dual():
 | |
|     """Tests init factory functions with dual main/alias factory functions"""
 | |
|     from pybind11_tests.factory_constructors import TestFactory7
 | |
| 
 | |
|     cstats = [TestFactory7.get_cstats(), TestFactory7.get_alias_cstats()]
 | |
|     cstats[0].alive()  # force gc
 | |
|     n_inst = ConstructorStats.detail_reg_inst()
 | |
| 
 | |
|     class PythFactory7(TestFactory7):
 | |
|         def get(self):
 | |
|             return 100 + TestFactory7.get(self)
 | |
| 
 | |
|     a1 = TestFactory7(1)
 | |
|     a2 = PythFactory7(2)
 | |
|     assert a1.get() == 1
 | |
|     assert a2.get() == 102
 | |
|     assert not a1.has_alias()
 | |
|     assert a2.has_alias()
 | |
| 
 | |
|     b1 = TestFactory7(tag.pointer, 3)
 | |
|     b2 = PythFactory7(tag.pointer, 4)
 | |
|     assert b1.get() == 3
 | |
|     assert b2.get() == 104
 | |
|     assert not b1.has_alias()
 | |
|     assert b2.has_alias()
 | |
| 
 | |
|     c1 = TestFactory7(tag.mixed, 5)
 | |
|     c2 = PythFactory7(tag.mixed, 6)
 | |
|     assert c1.get() == 5
 | |
|     assert c2.get() == 106
 | |
|     assert not c1.has_alias()
 | |
|     assert c2.has_alias()
 | |
| 
 | |
|     d1 = TestFactory7(tag.base, tag.pointer, 7)
 | |
|     d2 = PythFactory7(tag.base, tag.pointer, 8)
 | |
|     assert d1.get() == 7
 | |
|     assert d2.get() == 108
 | |
|     assert not d1.has_alias()
 | |
|     assert d2.has_alias()
 | |
| 
 | |
|     # Both return an alias; the second multiplies the value by 10:
 | |
|     e1 = TestFactory7(tag.alias, tag.pointer, 9)
 | |
|     e2 = PythFactory7(tag.alias, tag.pointer, 10)
 | |
|     assert e1.get() == 9
 | |
|     assert e2.get() == 200
 | |
|     assert e1.has_alias()
 | |
|     assert e2.has_alias()
 | |
| 
 | |
|     f1 = TestFactory7(tag.shared_ptr, tag.base, 11)
 | |
|     f2 = PythFactory7(tag.shared_ptr, tag.base, 12)
 | |
|     assert f1.get() == 11
 | |
|     assert f2.get() == 112
 | |
|     assert not f1.has_alias()
 | |
|     assert f2.has_alias()
 | |
| 
 | |
|     g1 = TestFactory7(tag.shared_ptr, tag.invalid_base, 13)
 | |
|     assert g1.get() == 13
 | |
|     assert not g1.has_alias()
 | |
|     with pytest.raises(TypeError) as excinfo:
 | |
|         PythFactory7(tag.shared_ptr, tag.invalid_base, 14)
 | |
|     assert (
 | |
|         str(excinfo.value)
 | |
|         == "pybind11::init(): construction failed: returned holder-wrapped instance is not an "
 | |
|         "alias instance"
 | |
|     )
 | |
| 
 | |
|     assert [i.alive() for i in cstats] == [13, 7]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 13
 | |
| 
 | |
|     del a1, a2, b1, d1, e1, e2
 | |
|     assert [i.alive() for i in cstats] == [7, 4]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst + 7
 | |
|     del b2, c1, c2, d2, f1, f2, g1
 | |
|     assert [i.alive() for i in cstats] == [0, 0]
 | |
|     assert ConstructorStats.detail_reg_inst() == n_inst
 | |
| 
 | |
|     assert [i.values() for i in cstats] == [
 | |
|         ["1", "2", "3", "4", "5", "6", "7", "8", "9", "100", "11", "12", "13", "14"],
 | |
|         ["2", "4", "6", "8", "9", "100", "12"],
 | |
|     ]
 | |
| 
 | |
| 
 | |
| def test_no_placement_new(capture):
 | |
|     """Prior to 2.2, `py::init<...>` relied on the type supporting placement
 | |
|     new; this tests a class without placement new support."""
 | |
|     with capture:
 | |
|         a = m.NoPlacementNew(123)
 | |
| 
 | |
|     found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
 | |
|     assert found
 | |
|     assert a.i == 123
 | |
|     with capture:
 | |
|         del a
 | |
|         pytest.gc_collect()
 | |
|     assert capture == "operator delete called on " + found.group(1)
 | |
| 
 | |
|     with capture:
 | |
|         b = m.NoPlacementNew()
 | |
| 
 | |
|     found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
 | |
|     assert found
 | |
|     assert b.i == 100
 | |
|     with capture:
 | |
|         del b
 | |
|         pytest.gc_collect()
 | |
|     assert capture == "operator delete called on " + found.group(1)
 | |
| 
 | |
| 
 | |
| def test_multiple_inheritance():
 | |
|     class MITest(m.TestFactory1, m.TestFactory2):
 | |
|         def __init__(self):
 | |
|             m.TestFactory1.__init__(self, tag.unique_ptr, 33)
 | |
|             m.TestFactory2.__init__(self, tag.move)
 | |
| 
 | |
|     a = MITest()
 | |
|     assert m.TestFactory1.value.fget(a) == "33"
 | |
|     assert m.TestFactory2.value.fget(a) == "(empty2)"
 | |
| 
 | |
| 
 | |
| def create_and_destroy(*args):
 | |
|     a = m.NoisyAlloc(*args)
 | |
|     print("---")
 | |
|     del a
 | |
|     pytest.gc_collect()
 | |
| 
 | |
| 
 | |
| def strip_comments(s):
 | |
|     return re.sub(r"\s+#.*", "", s)
 | |
| 
 | |
| 
 | |
| def test_reallocation_a(capture, msg):
 | |
|     """When the constructor is overloaded, previous overloads can require a preallocated value.
 | |
|     This test makes sure that such preallocated values only happen when they might be necessary,
 | |
|     and that they are deallocated properly."""
 | |
| 
 | |
|     pytest.gc_collect()
 | |
| 
 | |
|     with capture:
 | |
|         create_and_destroy(1)
 | |
|     assert (
 | |
|         msg(capture)
 | |
|         == """
 | |
|         noisy new
 | |
|         noisy placement new
 | |
|         NoisyAlloc(int 1)
 | |
|         ---
 | |
|         ~NoisyAlloc()
 | |
|         noisy delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_b(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(1.5)
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         noisy new               # allocation required to attempt first overload
 | |
|         noisy delete            # have to dealloc before considering factory init overload
 | |
|         noisy new               # pointer factory calling "new", part 1: allocation
 | |
|         NoisyAlloc(double 1.5)  # ... part two, invoking constructor
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_c(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(2, 3)
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         noisy new          # pointer factory calling "new", allocation
 | |
|         NoisyAlloc(int 2)  # constructor
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_d(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(2.5, 3)
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         NoisyAlloc(double 2.5)  # construction (local func variable: operator_new not called)
 | |
|         noisy new               # return-by-value "new" part 1: allocation
 | |
|         ~NoisyAlloc()           # moved-away local func variable destruction
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_e(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(3.5, 4.5)
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         noisy new               # preallocation needed before invoking placement-new overload
 | |
|         noisy placement new     # Placement new
 | |
|         NoisyAlloc(double 3.5)  # construction
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_f(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(4, 0.5)
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         noisy new          # preallocation needed before invoking placement-new overload
 | |
|         noisy delete       # deallocation of preallocated storage
 | |
|         noisy new          # Factory pointer allocation
 | |
|         NoisyAlloc(int 4)  # factory pointer construction
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_reallocation_g(capture, msg):
 | |
|     with capture:
 | |
|         create_and_destroy(5, "hi")
 | |
|     assert msg(capture) == strip_comments(
 | |
|         """
 | |
|         noisy new            # preallocation needed before invoking first placement new
 | |
|         noisy delete         # delete before considering new-style constructor
 | |
|         noisy new            # preallocation for second placement new
 | |
|         noisy placement new  # Placement new in the second placement new overload
 | |
|         NoisyAlloc(int 5)    # construction
 | |
|         ---
 | |
|         ~NoisyAlloc()  # Destructor
 | |
|         noisy delete   # operator delete
 | |
|     """
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_invalid_self():
 | |
|     """Tests invocation of the pybind-registered base class with an invalid `self` argument."""
 | |
| 
 | |
|     class NotPybindDerived:
 | |
|         pass
 | |
| 
 | |
|     # Attempts to initialize with an invalid type passed as `self`:
 | |
|     class BrokenTF1(m.TestFactory1):
 | |
|         def __init__(self, bad):
 | |
|             if bad == 1:
 | |
|                 a = m.TestFactory2(tag.pointer, 1)
 | |
|                 m.TestFactory1.__init__(a, tag.pointer)
 | |
|             elif bad == 2:
 | |
|                 a = NotPybindDerived()
 | |
|                 m.TestFactory1.__init__(a, tag.pointer)
 | |
| 
 | |
|     # Same as above, but for a class with an alias:
 | |
|     class BrokenTF6(m.TestFactory6):
 | |
|         def __init__(self, bad):
 | |
|             if bad == 0:
 | |
|                 m.TestFactory6.__init__()
 | |
|             elif bad == 1:
 | |
|                 a = m.TestFactory2(tag.pointer, 1)
 | |
|                 m.TestFactory6.__init__(a, tag.base, 1)
 | |
|             elif bad == 2:
 | |
|                 a = m.TestFactory2(tag.pointer, 1)
 | |
|                 m.TestFactory6.__init__(a, tag.alias, 1)
 | |
|             elif bad == 3:
 | |
|                 m.TestFactory6.__init__(
 | |
|                     NotPybindDerived.__new__(NotPybindDerived), tag.base, 1
 | |
|                 )
 | |
|             elif bad == 4:
 | |
|                 m.TestFactory6.__init__(
 | |
|                     NotPybindDerived.__new__(NotPybindDerived), tag.alias, 1
 | |
|                 )
 | |
| 
 | |
|     for arg in (1, 2):
 | |
|         with pytest.raises(TypeError) as excinfo:
 | |
|             BrokenTF1(arg)
 | |
|         assert (
 | |
|             str(excinfo.value)
 | |
|             == "__init__(self, ...) called with invalid or missing `self` argument"
 | |
|         )
 | |
| 
 | |
|     for arg in (0, 1, 2, 3, 4):
 | |
|         with pytest.raises(TypeError) as excinfo:
 | |
|             BrokenTF6(arg)
 | |
|         assert (
 | |
|             str(excinfo.value)
 | |
|             == "__init__(self, ...) called with invalid or missing `self` argument"
 | |
|         )
 |