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: object, arg1: int, arg2: object) 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: object, arg1: int, arg2: object) -> None """ ) 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" ) def test_factory_error_already_set(): obj = m.FactoryErrorAlreadySet(False) assert isinstance(obj, m.FactoryErrorAlreadySet) with pytest.raises(ValueError, match="factory sets error and returns nullptr"): m.FactoryErrorAlreadySet(True)