File indexing completed on 2025-01-18 10:17:54
0001 import re
0002
0003 import pytest
0004
0005 from pybind11_tests import ConstructorStats
0006 from pybind11_tests import factory_constructors as m
0007 from pybind11_tests.factory_constructors import tag
0008
0009
0010 def test_init_factory_basic():
0011 """Tests py::init_factory() wrapper around various ways of returning the object"""
0012
0013 cstats = [
0014 ConstructorStats.get(c)
0015 for c in [m.TestFactory1, m.TestFactory2, m.TestFactory3]
0016 ]
0017 cstats[0].alive()
0018 n_inst = ConstructorStats.detail_reg_inst()
0019
0020 x1 = m.TestFactory1(tag.unique_ptr, 3)
0021 assert x1.value == "3"
0022 y1 = m.TestFactory1(tag.pointer)
0023 assert y1.value == "(empty)"
0024 z1 = m.TestFactory1("hi!")
0025 assert z1.value == "hi!"
0026
0027 assert ConstructorStats.detail_reg_inst() == n_inst + 3
0028
0029 x2 = m.TestFactory2(tag.move)
0030 assert x2.value == "(empty2)"
0031 y2 = m.TestFactory2(tag.pointer, 7)
0032 assert y2.value == "7"
0033 z2 = m.TestFactory2(tag.unique_ptr, "hi again")
0034 assert z2.value == "hi again"
0035
0036 assert ConstructorStats.detail_reg_inst() == n_inst + 6
0037
0038 x3 = m.TestFactory3(tag.shared_ptr)
0039 assert x3.value == "(empty3)"
0040 y3 = m.TestFactory3(tag.pointer, 42)
0041 assert y3.value == "42"
0042 z3 = m.TestFactory3("bye")
0043 assert z3.value == "bye"
0044
0045 for null_ptr_kind in [tag.null_ptr, tag.null_unique_ptr, tag.null_shared_ptr]:
0046 with pytest.raises(TypeError) as excinfo:
0047 m.TestFactory3(null_ptr_kind)
0048 assert (
0049 str(excinfo.value) == "pybind11::init(): factory function returned nullptr"
0050 )
0051
0052 assert [i.alive() for i in cstats] == [3, 3, 3]
0053 assert ConstructorStats.detail_reg_inst() == n_inst + 9
0054
0055 del x1, y2, y3, z3
0056 assert [i.alive() for i in cstats] == [2, 2, 1]
0057 assert ConstructorStats.detail_reg_inst() == n_inst + 5
0058 del x2, x3, y1, z1, z2
0059 assert [i.alive() for i in cstats] == [0, 0, 0]
0060 assert ConstructorStats.detail_reg_inst() == n_inst
0061
0062 assert [i.values() for i in cstats] == [
0063 ["3", "hi!"],
0064 ["7", "hi again"],
0065 ["42", "bye"],
0066 ]
0067 assert [i.default_constructions for i in cstats] == [1, 1, 1]
0068
0069
0070 def test_init_factory_signature(msg):
0071 with pytest.raises(TypeError) as excinfo:
0072 m.TestFactory1("invalid", "constructor", "arguments")
0073 assert (
0074 msg(excinfo.value)
0075 == """
0076 __init__(): incompatible constructor arguments. The following argument types are supported:
0077 1. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int)
0078 2. m.factory_constructors.TestFactory1(arg0: str)
0079 3. m.factory_constructors.TestFactory1(arg0: m.factory_constructors.tag.pointer_tag)
0080 4. m.factory_constructors.TestFactory1(arg0: handle, arg1: int, arg2: handle)
0081
0082 Invoked with: 'invalid', 'constructor', 'arguments'
0083 """
0084 )
0085
0086 assert (
0087 msg(m.TestFactory1.__init__.__doc__)
0088 == """
0089 __init__(*args, **kwargs)
0090 Overloaded function.
0091
0092 1. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.unique_ptr_tag, arg1: int) -> None
0093
0094 2. __init__(self: m.factory_constructors.TestFactory1, arg0: str) -> None
0095
0096 3. __init__(self: m.factory_constructors.TestFactory1, arg0: m.factory_constructors.tag.pointer_tag) -> None
0097
0098 4. __init__(self: m.factory_constructors.TestFactory1, arg0: handle, arg1: int, arg2: handle) -> None
0099 """
0100 )
0101
0102
0103 def test_init_factory_casting():
0104 """Tests py::init_factory() wrapper with various upcasting and downcasting returns"""
0105
0106 cstats = [
0107 ConstructorStats.get(c)
0108 for c in [m.TestFactory3, m.TestFactory4, m.TestFactory5]
0109 ]
0110 cstats[0].alive()
0111 n_inst = ConstructorStats.detail_reg_inst()
0112
0113
0114 a = m.TestFactory3(tag.pointer, tag.TF4, 4)
0115 assert a.value == "4"
0116 b = m.TestFactory3(tag.shared_ptr, tag.TF4, 5)
0117 assert b.value == "5"
0118 c = m.TestFactory3(tag.pointer, tag.TF5, 6)
0119 assert c.value == "6"
0120 d = m.TestFactory3(tag.shared_ptr, tag.TF5, 7)
0121 assert d.value == "7"
0122
0123 assert ConstructorStats.detail_reg_inst() == n_inst + 4
0124
0125
0126 e = m.TestFactory4(tag.pointer, tag.TF4, 8)
0127 assert e.value == "8"
0128
0129 assert ConstructorStats.detail_reg_inst() == n_inst + 5
0130 assert [i.alive() for i in cstats] == [5, 3, 2]
0131
0132 del a
0133 assert [i.alive() for i in cstats] == [4, 2, 2]
0134 assert ConstructorStats.detail_reg_inst() == n_inst + 4
0135
0136 del b, c, e
0137 assert [i.alive() for i in cstats] == [1, 0, 1]
0138 assert ConstructorStats.detail_reg_inst() == n_inst + 1
0139
0140 del d
0141 assert [i.alive() for i in cstats] == [0, 0, 0]
0142 assert ConstructorStats.detail_reg_inst() == n_inst
0143
0144 assert [i.values() for i in cstats] == [
0145 ["4", "5", "6", "7", "8"],
0146 ["4", "5", "8"],
0147 ["6", "7"],
0148 ]
0149
0150
0151 def test_init_factory_alias():
0152 """Tests py::init_factory() wrapper with value conversions and alias types"""
0153
0154 cstats = [m.TestFactory6.get_cstats(), m.TestFactory6.get_alias_cstats()]
0155 cstats[0].alive()
0156 n_inst = ConstructorStats.detail_reg_inst()
0157
0158 a = m.TestFactory6(tag.base, 1)
0159 assert a.get() == 1
0160 assert not a.has_alias()
0161 b = m.TestFactory6(tag.alias, "hi there")
0162 assert b.get() == 8
0163 assert b.has_alias()
0164 c = m.TestFactory6(tag.alias, 3)
0165 assert c.get() == 3
0166 assert c.has_alias()
0167 d = m.TestFactory6(tag.alias, tag.pointer, 4)
0168 assert d.get() == 4
0169 assert d.has_alias()
0170 e = m.TestFactory6(tag.base, tag.pointer, 5)
0171 assert e.get() == 5
0172 assert not e.has_alias()
0173 f = m.TestFactory6(tag.base, tag.alias, tag.pointer, 6)
0174 assert f.get() == 6
0175 assert f.has_alias()
0176
0177 assert ConstructorStats.detail_reg_inst() == n_inst + 6
0178 assert [i.alive() for i in cstats] == [6, 4]
0179
0180 del a, b, e
0181 assert [i.alive() for i in cstats] == [3, 3]
0182 assert ConstructorStats.detail_reg_inst() == n_inst + 3
0183 del f, c, d
0184 assert [i.alive() for i in cstats] == [0, 0]
0185 assert ConstructorStats.detail_reg_inst() == n_inst
0186
0187 class MyTest(m.TestFactory6):
0188 def __init__(self, *args):
0189 m.TestFactory6.__init__(self, *args)
0190
0191 def get(self):
0192 return -5 + m.TestFactory6.get(self)
0193
0194
0195 z = MyTest(tag.base, 123)
0196 assert z.get() == 118
0197 assert z.has_alias()
0198
0199
0200 y = MyTest(tag.alias, "why hello!")
0201 assert y.get() == 5
0202 assert y.has_alias()
0203
0204
0205 x = MyTest(tag.base, tag.pointer, 47)
0206 assert x.get() == 42
0207 assert x.has_alias()
0208
0209 assert ConstructorStats.detail_reg_inst() == n_inst + 3
0210 assert [i.alive() for i in cstats] == [3, 3]
0211 del x, y, z
0212 assert [i.alive() for i in cstats] == [0, 0]
0213 assert ConstructorStats.detail_reg_inst() == n_inst
0214
0215 assert [i.values() for i in cstats] == [
0216 ["1", "8", "3", "4", "5", "6", "123", "10", "47"],
0217 ["hi there", "3", "4", "6", "move", "123", "why hello!", "move", "47"],
0218 ]
0219
0220
0221 def test_init_factory_dual():
0222 """Tests init factory functions with dual main/alias factory functions"""
0223 from pybind11_tests.factory_constructors import TestFactory7
0224
0225 cstats = [TestFactory7.get_cstats(), TestFactory7.get_alias_cstats()]
0226 cstats[0].alive()
0227 n_inst = ConstructorStats.detail_reg_inst()
0228
0229 class PythFactory7(TestFactory7):
0230 def get(self):
0231 return 100 + TestFactory7.get(self)
0232
0233 a1 = TestFactory7(1)
0234 a2 = PythFactory7(2)
0235 assert a1.get() == 1
0236 assert a2.get() == 102
0237 assert not a1.has_alias()
0238 assert a2.has_alias()
0239
0240 b1 = TestFactory7(tag.pointer, 3)
0241 b2 = PythFactory7(tag.pointer, 4)
0242 assert b1.get() == 3
0243 assert b2.get() == 104
0244 assert not b1.has_alias()
0245 assert b2.has_alias()
0246
0247 c1 = TestFactory7(tag.mixed, 5)
0248 c2 = PythFactory7(tag.mixed, 6)
0249 assert c1.get() == 5
0250 assert c2.get() == 106
0251 assert not c1.has_alias()
0252 assert c2.has_alias()
0253
0254 d1 = TestFactory7(tag.base, tag.pointer, 7)
0255 d2 = PythFactory7(tag.base, tag.pointer, 8)
0256 assert d1.get() == 7
0257 assert d2.get() == 108
0258 assert not d1.has_alias()
0259 assert d2.has_alias()
0260
0261
0262 e1 = TestFactory7(tag.alias, tag.pointer, 9)
0263 e2 = PythFactory7(tag.alias, tag.pointer, 10)
0264 assert e1.get() == 9
0265 assert e2.get() == 200
0266 assert e1.has_alias()
0267 assert e2.has_alias()
0268
0269 f1 = TestFactory7(tag.shared_ptr, tag.base, 11)
0270 f2 = PythFactory7(tag.shared_ptr, tag.base, 12)
0271 assert f1.get() == 11
0272 assert f2.get() == 112
0273 assert not f1.has_alias()
0274 assert f2.has_alias()
0275
0276 g1 = TestFactory7(tag.shared_ptr, tag.invalid_base, 13)
0277 assert g1.get() == 13
0278 assert not g1.has_alias()
0279 with pytest.raises(TypeError) as excinfo:
0280 PythFactory7(tag.shared_ptr, tag.invalid_base, 14)
0281 assert (
0282 str(excinfo.value)
0283 == "pybind11::init(): construction failed: returned holder-wrapped instance is not an "
0284 "alias instance"
0285 )
0286
0287 assert [i.alive() for i in cstats] == [13, 7]
0288 assert ConstructorStats.detail_reg_inst() == n_inst + 13
0289
0290 del a1, a2, b1, d1, e1, e2
0291 assert [i.alive() for i in cstats] == [7, 4]
0292 assert ConstructorStats.detail_reg_inst() == n_inst + 7
0293 del b2, c1, c2, d2, f1, f2, g1
0294 assert [i.alive() for i in cstats] == [0, 0]
0295 assert ConstructorStats.detail_reg_inst() == n_inst
0296
0297 assert [i.values() for i in cstats] == [
0298 ["1", "2", "3", "4", "5", "6", "7", "8", "9", "100", "11", "12", "13", "14"],
0299 ["2", "4", "6", "8", "9", "100", "12"],
0300 ]
0301
0302
0303 def test_no_placement_new(capture):
0304 """Prior to 2.2, `py::init<...>` relied on the type supporting placement
0305 new; this tests a class without placement new support."""
0306 with capture:
0307 a = m.NoPlacementNew(123)
0308
0309 found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
0310 assert found
0311 assert a.i == 123
0312 with capture:
0313 del a
0314 pytest.gc_collect()
0315 assert capture == "operator delete called on " + found.group(1)
0316
0317 with capture:
0318 b = m.NoPlacementNew()
0319
0320 found = re.search(r"^operator new called, returning (\d+)\n$", str(capture))
0321 assert found
0322 assert b.i == 100
0323 with capture:
0324 del b
0325 pytest.gc_collect()
0326 assert capture == "operator delete called on " + found.group(1)
0327
0328
0329 def test_multiple_inheritance():
0330 class MITest(m.TestFactory1, m.TestFactory2):
0331 def __init__(self):
0332 m.TestFactory1.__init__(self, tag.unique_ptr, 33)
0333 m.TestFactory2.__init__(self, tag.move)
0334
0335 a = MITest()
0336 assert m.TestFactory1.value.fget(a) == "33"
0337 assert m.TestFactory2.value.fget(a) == "(empty2)"
0338
0339
0340 def create_and_destroy(*args):
0341 a = m.NoisyAlloc(*args)
0342 print("---")
0343 del a
0344 pytest.gc_collect()
0345
0346
0347 def strip_comments(s):
0348 return re.sub(r"\s+#.*", "", s)
0349
0350
0351 def test_reallocation_a(capture, msg):
0352 """When the constructor is overloaded, previous overloads can require a preallocated value.
0353 This test makes sure that such preallocated values only happen when they might be necessary,
0354 and that they are deallocated properly."""
0355
0356 pytest.gc_collect()
0357
0358 with capture:
0359 create_and_destroy(1)
0360 assert (
0361 msg(capture)
0362 == """
0363 noisy new
0364 noisy placement new
0365 NoisyAlloc(int 1)
0366 ---
0367 ~NoisyAlloc()
0368 noisy delete
0369 """
0370 )
0371
0372
0373 def test_reallocation_b(capture, msg):
0374 with capture:
0375 create_and_destroy(1.5)
0376 assert msg(capture) == strip_comments(
0377 """
0378 noisy new # allocation required to attempt first overload
0379 noisy delete # have to dealloc before considering factory init overload
0380 noisy new # pointer factory calling "new", part 1: allocation
0381 NoisyAlloc(double 1.5) # ... part two, invoking constructor
0382 ---
0383 ~NoisyAlloc() # Destructor
0384 noisy delete # operator delete
0385 """
0386 )
0387
0388
0389 def test_reallocation_c(capture, msg):
0390 with capture:
0391 create_and_destroy(2, 3)
0392 assert msg(capture) == strip_comments(
0393 """
0394 noisy new # pointer factory calling "new", allocation
0395 NoisyAlloc(int 2) # constructor
0396 ---
0397 ~NoisyAlloc() # Destructor
0398 noisy delete # operator delete
0399 """
0400 )
0401
0402
0403 def test_reallocation_d(capture, msg):
0404 with capture:
0405 create_and_destroy(2.5, 3)
0406 assert msg(capture) == strip_comments(
0407 """
0408 NoisyAlloc(double 2.5) # construction (local func variable: operator_new not called)
0409 noisy new # return-by-value "new" part 1: allocation
0410 ~NoisyAlloc() # moved-away local func variable destruction
0411 ---
0412 ~NoisyAlloc() # Destructor
0413 noisy delete # operator delete
0414 """
0415 )
0416
0417
0418 def test_reallocation_e(capture, msg):
0419 with capture:
0420 create_and_destroy(3.5, 4.5)
0421 assert msg(capture) == strip_comments(
0422 """
0423 noisy new # preallocation needed before invoking placement-new overload
0424 noisy placement new # Placement new
0425 NoisyAlloc(double 3.5) # construction
0426 ---
0427 ~NoisyAlloc() # Destructor
0428 noisy delete # operator delete
0429 """
0430 )
0431
0432
0433 def test_reallocation_f(capture, msg):
0434 with capture:
0435 create_and_destroy(4, 0.5)
0436 assert msg(capture) == strip_comments(
0437 """
0438 noisy new # preallocation needed before invoking placement-new overload
0439 noisy delete # deallocation of preallocated storage
0440 noisy new # Factory pointer allocation
0441 NoisyAlloc(int 4) # factory pointer construction
0442 ---
0443 ~NoisyAlloc() # Destructor
0444 noisy delete # operator delete
0445 """
0446 )
0447
0448
0449 def test_reallocation_g(capture, msg):
0450 with capture:
0451 create_and_destroy(5, "hi")
0452 assert msg(capture) == strip_comments(
0453 """
0454 noisy new # preallocation needed before invoking first placement new
0455 noisy delete # delete before considering new-style constructor
0456 noisy new # preallocation for second placement new
0457 noisy placement new # Placement new in the second placement new overload
0458 NoisyAlloc(int 5) # construction
0459 ---
0460 ~NoisyAlloc() # Destructor
0461 noisy delete # operator delete
0462 """
0463 )
0464
0465
0466 def test_invalid_self():
0467 """Tests invocation of the pybind-registered base class with an invalid `self` argument."""
0468
0469 class NotPybindDerived:
0470 pass
0471
0472
0473 class BrokenTF1(m.TestFactory1):
0474 def __init__(self, bad):
0475 if bad == 1:
0476 a = m.TestFactory2(tag.pointer, 1)
0477 m.TestFactory1.__init__(a, tag.pointer)
0478 elif bad == 2:
0479 a = NotPybindDerived()
0480 m.TestFactory1.__init__(a, tag.pointer)
0481
0482
0483 class BrokenTF6(m.TestFactory6):
0484 def __init__(self, bad):
0485 if bad == 0:
0486 m.TestFactory6.__init__()
0487 elif bad == 1:
0488 a = m.TestFactory2(tag.pointer, 1)
0489 m.TestFactory6.__init__(a, tag.base, 1)
0490 elif bad == 2:
0491 a = m.TestFactory2(tag.pointer, 1)
0492 m.TestFactory6.__init__(a, tag.alias, 1)
0493 elif bad == 3:
0494 m.TestFactory6.__init__(
0495 NotPybindDerived.__new__(NotPybindDerived), tag.base, 1
0496 )
0497 elif bad == 4:
0498 m.TestFactory6.__init__(
0499 NotPybindDerived.__new__(NotPybindDerived), tag.alias, 1
0500 )
0501
0502 for arg in (1, 2):
0503 with pytest.raises(TypeError) as excinfo:
0504 BrokenTF1(arg)
0505 assert (
0506 str(excinfo.value)
0507 == "__init__(self, ...) called with invalid or missing `self` argument"
0508 )
0509
0510 for arg in (0, 1, 2, 3, 4):
0511 with pytest.raises(TypeError) as excinfo:
0512 BrokenTF6(arg)
0513 assert (
0514 str(excinfo.value)
0515 == "__init__(self, ...) called with invalid or missing `self` argument"
0516 )